Skip to content

Commit

Permalink
AST support for include aliasing
Browse files Browse the repository at this point in the history
Summary:
Support include aliases in the thrift AST.

Include aliasing takes the form of:

```thrift
include "a/b/c/my_program.thrift" as my_alias

struct Foo {
    1: my_alias.Bar my_field;
}
```

Where each program will only allow resolution of scope aliases defined locally within the program.

Reviewed By: iahs

Differential Revision: D68747918

fbshipit-source-id: 5b8d021f5e6d1d5a3b8c9ca754a51ccd778d9172
  • Loading branch information
Sam De Roeck authored and facebook-github-bot committed Feb 13, 2025
1 parent 712c290 commit 74bf0b9
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 71 deletions.
14 changes: 11 additions & 3 deletions third-party/thrift/src/thrift/compiler/ast/t_include.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,27 @@ class t_program;
*/
class t_include : public t_node {
public:
t_include(t_program* program, std::string raw_path)
: program_(program), raw_path_(std::move(raw_path)) {}
t_include(
t_program* program,
std::string raw_path,
std::optional<std::string> alias)
: program_(program),
raw_path_(std::move(raw_path)),
alias_(std::move(alias)) {}

t_program* get_program() const { return program_; }

fmt::string_view raw_path() const { return raw_path_; }
std::string_view raw_path() const { return raw_path_; }

std::optional<std::string_view> alias() const { return alias_; }

void set_str_range(source_range rng) { range_ = rng; }
source_range str_range() const { return range_; }

private:
t_program* program_;
std::string raw_path_;
std::optional<std::string> alias_;
source_range range_;
};

Expand Down
171 changes: 106 additions & 65 deletions third-party/thrift/src/thrift/compiler/parse/parse_ast.cc
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,10 @@ class parsing_terminator : public std::runtime_error {
}

using include_handler = std::function<t_program*(
source_range range, const std::string& include_name, const t_program& p)>;
source_range range,
const std::string& include_name,
const std::optional<std::string_view>& alias,
const t_program& p)>;

// A semantic analyzer and AST builder for a single Thrift program.
class ast_builder : public parser_actions {
Expand Down Expand Up @@ -505,14 +508,20 @@ class ast_builder : public parser_actions {
void on_include(
source_range range,
std::string_view str,
const std::optional<std::string_view>& alias,
source_range str_range) override {
std::string include_name = fmt::to_string(str);
auto included_program = on_include_(range, include_name, program_);
std::optional<std::string> alias_str_opt = alias.has_value()
? std::make_optional(std::string{alias.value()})
: std::nullopt;
auto included_program =
on_include_(range, include_name, alias_str_opt, program_);
auto last_slash = include_name.find_last_of("/\\");
if (last_slash != std::string::npos) {
included_program->set_include_prefix(include_name.substr(0, last_slash));
}
auto include = std::make_unique<t_include>(included_program, include_name);
auto include = std::make_unique<t_include>(
included_program, std::move(include_name), std::move(alias_str_opt));
include->set_src_range(range);
include->set_str_range(str_range);
program_.add_include(std::move(include));
Expand Down Expand Up @@ -1014,73 +1023,105 @@ std::unique_ptr<t_program_bundle> parse_ast(

auto circular_deps = std::set<std::string>{path};

include_handler on_include = [&](source_range range,
const std::string& include_path,
const t_program& parent) {
auto path_or_error = sm.find_include_file(
include_path, parent.path(), params.incl_searchpath);
if (path_or_error.index() == 1) {
diags.report(
range.begin,
params.allow_missing_includes ? diagnostic_level::warning
: diagnostic_level::error,
"{}",
std::get<1>(path_or_error));
if (!params.allow_missing_includes) {
end_parsing();
}
}
include_handler on_include =
[&](source_range range,
const std::string& include_path,
const std::optional<std::string_view>& include_alias,
const t_program& parent) {
auto path_or_error = sm.find_include_file(
include_path, parent.path(), params.incl_searchpath);
if (path_or_error.index() == 1) {
diags.report(
range.begin,
params.allow_missing_includes ? diagnostic_level::warning
: diagnostic_level::error,
"{}",
std::get<1>(path_or_error));
if (!params.allow_missing_includes) {
end_parsing();
}
}

// Skip already parsed files.
t_program* program = nullptr;
const std::string* resolved_path = &include_path;
const std::string* full_path = resolved_path;
if (path_or_error.index() == 0) {
full_path = &std::get<0>(path_or_error);
program = programs->find_program_by_full_path(*full_path);
if (program) {
// We've already seen this program but know it by another path.
resolved_path = &program->path();
}
}
if (program) {
if (program == programs->get_root_program()) {
// If we're including the root program we must have a dependency cycle.
assert(circular_deps.count(*full_path));
} else {
return program;
}
}
// Resolve the include path to a full path.
t_program* program = nullptr;
const std::string* resolved_path = &include_path;
const std::string* full_path = resolved_path;
if (path_or_error.index() == 0) {
full_path = &std::get<0>(path_or_error);
program = programs->find_program_by_full_path(*full_path);
if (program) {
// We've already seen this program but know it by another path.
resolved_path = &program->path();
}
}

// Fail on circular dependencies.
if (!circular_deps.insert(*full_path).second) {
diags.error(
range.begin,
"Circular dependency found: file `{}` is already parsed.",
*resolved_path);
end_parsing();
}
// Fail on duplicate include aliases
if (include_alias.has_value()) {
const auto& knownIncludes = parent.includes();
for (const t_include* inc : knownIncludes) {
if (!inc->alias()) {
continue;
}

if (inc->alias() == include_alias.value()) {
diags.error(
range.begin,
"'{}' is already an alias for '{}'",
*include_alias,
inc->get_program()->full_path());
}

if (*full_path == inc->get_program()->full_path()) {
diags.error(
range.begin,
"Include '{}' has multiple aliases: '{}' vs '{}'",
*full_path,
inc->alias().value(),
*include_alias);
}
}
}

// Create a new program for a Thrift file in an include statement and
// set its include_prefix by parsing the directory which it is
// included from.
auto included_program =
std::make_unique<t_program>(*resolved_path, *full_path, &parent);
program = included_program.get();
programs->add_program(std::move(included_program));
// Skip already parsed files.
if (program) {
if (program == programs->get_root_program()) {
// If we're including the root program we must have a dependency
// cycle.
assert(circular_deps.count(*full_path));
} else {
return program;
}
}

try {
ast_builder(diags, *program, params, on_include)
.parse_file(sm, range.begin);
} catch (...) {
if (!params.allow_missing_includes) {
throw;
}
}
// Fail on circular dependencies.
if (!circular_deps.insert(*full_path).second) {
diags.error(
range.begin,
"Circular dependency found: file `{}` is already parsed.",
*resolved_path);
end_parsing();
}

circular_deps.erase(*full_path);
return program;
};
// Create a new program for a Thrift file in an include statement and
// set its include_prefix by parsing the directory which it is
// included from.
auto included_program =
std::make_unique<t_program>(*resolved_path, *full_path, &parent);
program = included_program.get();
programs->add_program(std::move(included_program));

try {
ast_builder(diags, *program, params, on_include)
.parse_file(sm, range.begin);
} catch (...) {
if (!params.allow_missing_includes) {
throw;
}
}

circular_deps.erase(*full_path);
return program;
};

t_program& root_program = *programs->root_program();
try {
Expand Down
2 changes: 1 addition & 1 deletion third-party/thrift/src/thrift/compiler/parse/parser.cc
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ class parser {
consume_token();
switch (kind) {
case tok::kw_include:
actions_.on_include(range, str, str_range);
actions_.on_include(range, str, std::nullopt, str_range);
break;
case tok::kw_cpp_include:
actions_.on_cpp_include(range, str);
Expand Down
5 changes: 4 additions & 1 deletion third-party/thrift/src/thrift/compiler/parse/parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@ class parser_actions {
std::string_view name) = 0;

virtual void on_include(
source_range range, std::string_view str, source_range str_range) = 0;
source_range range,
std::string_view str,
const std::optional<std::string_view>& alias,
source_range str_range) = 0;
virtual void on_cpp_include(source_range range, std::string_view str) = 0;
virtual void on_hs_include(source_range range, std::string_view str) = 0;

Expand Down
3 changes: 2 additions & 1 deletion third-party/thrift/src/thrift/compiler/parse/token.cc
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ constexpr token_kind_info info[] = {
THRIFT_KEYWORD(struct),
THRIFT_KEYWORD(throws),
THRIFT_KEYWORD(typedef),
THRIFT_KEYWORD(union)};
THRIFT_KEYWORD(union),
THRIFT_KEYWORD(as)};

constexpr int num_token_kinds = sizeof(info) / sizeof(*info);

Expand Down
1 change: 1 addition & 0 deletions third-party/thrift/src/thrift/compiler/parse/token.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ enum class tok {
kw_throws,
kw_typedef,
kw_union,
kw_as,
};

namespace detail {
Expand Down

0 comments on commit 74bf0b9

Please sign in to comment.