thrift/compiler/compiler.cc (381 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * thrift - a lightweight cross-language rpc/serialization tool * * This file contains the main compiler engine for Thrift, which invokes the * scanner/parser to build the thrift object tree. The interface generation * code for each language lives in a file by the language name under the * generate/ folder, and all parse structures live in parse/ * */ #include <thrift/compiler/compiler.h> #ifdef _WIN32 #include <process.h> // @manual #else #include <unistd.h> #endif #include <ctime> #include <boost/algorithm/string/join.hpp> #include <boost/algorithm/string/split.hpp> #include <boost/filesystem.hpp> #include <thrift/compiler/ast/diagnostic.h> #include <thrift/compiler/ast/diagnostic_context.h> #include <thrift/compiler/common.h> #include <thrift/compiler/generate/t_generator.h> #include <thrift/compiler/mutator/mutator.h> #include <thrift/compiler/parse/parsing_driver.h> #include <thrift/compiler/platform.h> #include <thrift/compiler/sema/standard_validator.h> #include <thrift/compiler/validator/validator.h> namespace apache { namespace thrift { namespace compiler { namespace { /** * Flags to control code generation */ struct gen_params { bool gen_recurse = false; std::string genfile; std::vector<std::string> targets; t_generation_context context; }; /** * Display the usage message. */ void usage() { fprintf(stderr, "Usage: thrift [options] file\n"); fprintf(stderr, "Options:\n"); fprintf( stderr, " -o dir Set the output directory for gen-* packages\n" " (default: current directory)\n"); fprintf( stderr, " -out dir Set the output location for generated files\n" " (no gen-* folder will be created)\n"); fprintf( stderr, " -I dir Add a directory to the list of directories\n" " searched for include directives\n"); fprintf(stderr, " -nowarn Suppress all compiler warnings (BAD!)\n"); fprintf(stderr, " -strict Strict compiler warnings on\n"); fprintf(stderr, " -v[erbose] Verbose mode\n"); fprintf(stderr, " -r[ecurse] Also generate included files\n"); fprintf(stderr, " -debug Parse debug trace to stdout\n"); fprintf( stderr, " --allow-neg-keys Allow negative field keys (Used to preserve protocol\n" " compatibility with older .thrift files)\n"); fprintf(stderr, " --allow-neg-enum-vals Allow negative enum vals\n"); fprintf( stderr, " --allow-64bit-consts Do not print warnings about using 64-bit constants\n"); fprintf( stderr, " --allow-experimental-features feature[,feature]\n" " Enable experimental features. Use 'all' to enable all experimental features.\n"); fprintf( stderr, " --gen STR Generate code with a dynamically-registered generator.\n" " STR has the form language[:key1=val1[,key2,[key3=val3]]].\n" " Keys and values are options passed to the generator.\n" " Many options will not require values.\n"); fprintf( stderr, " --record-genfiles FILE\n" " Save the list of generated files to FILE,\n"); fprintf(stderr, "\n"); fprintf(stderr, "Available generators (and options):\n"); t_generator_registry::gen_map_t gen_map = t_generator_registry::get_generator_map(); t_generator_registry::gen_map_t::iterator iter; for (iter = gen_map.begin(); iter != gen_map.end(); ++iter) { fprintf( stderr, " %s (%s):\n", iter->second->get_short_name().c_str(), iter->second->get_long_name().c_str()); fprintf(stderr, "%s", iter->second->get_documentation().c_str()); } } bool isPathSeparator(const char& c) { #ifdef _WIN32 return c == ';'; #else return c == ':'; #endif } bool isComma(const char& c) { return c == ','; } // Returns the input file name if successful, otherwise returns an empty // string. std::string parseArgs( const std::vector<std::string>& arguments, parsing_params& pparams, gen_params& gparams, diagnostic_params& dparams) { // Check for necessary arguments, you gotta have at least a filename and // an output language flag. if (arguments.size() < 2) { usage(); return {}; } // A helper that grabs the next argument, if possible. // Outputs an error and returns nullptr if not. size_t arg_i = 1; // Skip the binary name. auto consume_arg = [&](const char* arg_name) -> const std::string* { // Note: The input filename must be the last argument. if (arg_i + 2 >= arguments.size()) { fprintf( stderr, "!!! Missing %s between %s and '%s'\n", arg_name, arguments[arg_i].c_str(), arguments[arg_i + 1].c_str()); usage(); return nullptr; } return &arguments[++arg_i]; }; // Hacky parameter handling... I didn't feel like using a library sorry! bool nowarn = false; // Guard so --nowarn and --strict are order agnostic. for (; arg_i < arguments.size() - 1; ++arg_i) { // Last argument is the src file. // Parse flag. std::string flag; if (arguments[arg_i].size() < 2 || arguments[arg_i][0] != '-') { fprintf(stderr, "!!! Expected flag, got: %s\n", arguments[arg_i].c_str()); usage(); return {}; } else if (arguments[arg_i][1] == '-') { flag = arguments[arg_i].substr(2); } else { flag = arguments[arg_i].substr(1); } // Interpret flag. if (flag == "allow-experimental-features") { auto* arg = consume_arg("feature"); if (arg == nullptr) { return {}; } boost::algorithm::split( pparams.allow_experimental_features, *arg, isComma); } else if (flag == "debug") { dparams.debug = true; g_debug = 1; } else if (flag == "nowarn") { dparams.warn_level = g_warn = 0; nowarn = true; } else if (flag == "strict") { pparams.strict = 255; if (!nowarn) { // Don't override nowarn. dparams.warn_level = g_warn = 2; } } else if (flag == "v" || flag == "verbose") { dparams.info = true; g_verbose = 1; } else if (flag == "r" || flag == "recurse") { gparams.gen_recurse = true; } else if (flag == "allow-neg-keys") { pparams.allow_neg_field_keys = true; } else if (flag == "allow-neg-enum-vals") { dparams.allow_neg_enum_vals = true; } else if (flag == "allow-64bit-consts") { pparams.allow_64bit_consts = true; } else if (flag == "record-genfiles") { auto* arg = consume_arg("genfile file specification"); if (arg == nullptr) { return {}; } gparams.genfile = *arg; } else if (flag == "gen") { auto* arg = consume_arg("generator specification"); if (arg == nullptr) { return {}; } gparams.targets.push_back(*arg); } else if (flag == "cpp_use_include_prefix") { g_cpp_use_include_prefix = true; } else if (flag == "I") { auto* arg = consume_arg("include directory"); if (arg == nullptr) { return {}; } // An argument of "-I\ asdf" is invalid and has unknown results pparams.incl_searchpath.push_back(*arg); } else if (flag == "o" || flag == "out") { auto* arg = consume_arg("output directory"); if (arg == nullptr) { return {}; } std::string out_path = *arg; bool out_path_is_absolute = (flag == "out"); // Strip out trailing \ on a Windows path if (platform_is_windows()) { int last = out_path.length() - 1; if (out_path[last] == '\\') { out_path.erase(last); } } if (out_path_is_absolute) { // Invoker specified `-out blah`. We are supposed to output directly // into blah, e.g. `blah/Foo.java`. Make the directory if necessary, // just like how for `-o blah` we make `o/gen-java` boost::system::error_code errc; boost::filesystem::create_directory(out_path, errc); if (errc) { fprintf( stderr, "Output path %s is unusable or not a directory\n", out_path.c_str()); return {}; } } if (!boost::filesystem::is_directory(out_path)) { fprintf( stderr, "Output path %s is unusable or not a directory\n", out_path.c_str()); return {}; } gparams.context = {std::move(out_path), out_path_is_absolute}; } else { fprintf( stderr, "!!! Unrecognized option: %s\n", arguments[arg_i].c_str()); usage(); return {}; } } if (const char* env_p = std::getenv("THRIFT_INCLUDE_PATH")) { std::vector<std::string> components; boost::algorithm::split(components, env_p, isPathSeparator); pparams.incl_searchpath.insert( pparams.incl_searchpath.end(), components.begin(), components.end()); } // You gotta generate something! if (gparams.targets.empty()) { fprintf(stderr, "!!! No output language(s) specified\n\n"); usage(); return {}; } // Return the input file name. assert(arg_i == arguments.size() - 1); return arguments[arg_i]; } /** * Generate code */ bool generate( const gen_params& params, t_program* program, std::set<std::string>& already_generated) { // Oooohh, recursive code generation, hot!! if (params.gen_recurse) { // Add the path we are about to generate. already_generated.emplace(program->path()); for (const auto& include : program->get_included_programs()) { if (!already_generated.count(include->path()) && !generate(params, include, already_generated)) { return false; } } } // Generate code! try { pverbose("Program: %s\n", program->path().c_str()); if (dump_docs) { dump_docstrings(program); } bool success = true; std::ofstream genfile; if (!params.genfile.empty()) { genfile.open(params.genfile); } for (auto target : params.targets) { auto pos = target.find(':'); std::string lang = target.substr(0, pos); auto generator = std::unique_ptr<t_generator>{ t_generator_registry::get_generator(program, params.context, target)}; if (generator == nullptr) { continue; } validator::diagnostics_t diagnostics; validator_list validators(diagnostics); generator->fill_validator_list(validators); validators.traverse(program); bool has_failure = false; for (const auto& d : diagnostics) { has_failure = has_failure || (d.level() == diagnostic_level::failure); std::cerr << d << std::endl; } if (has_failure) { success = false; continue; } pverbose("Generating \"%s\"\n", target.c_str()); generator->generate_program(); if (genfile.is_open()) { for (const std::string& s : generator->get_genfiles()) { genfile << s << "\n"; } } } return success; } catch (const std::string& s) { printf("Error: %s\n", s.c_str()); return false; } catch (const char* exc) { printf("Error: %s\n", exc); return false; } } bool generate(const gen_params& params, t_program* program) { std::set<std::string> already_generated; return generate(params, program, already_generated); } std::string get_include_path( const std::vector<std::string>& generator_targets, const std::string& input_filename) { std::string include_prefix; for (const auto& target : generator_targets) { auto const colon_pos = target.find(':'); if (colon_pos == std::string::npos) { continue; } auto const lang_name = target.substr(0, colon_pos); if (lang_name != "cpp2" && lang_name != "mstch_cpp2") { continue; } auto const lang_args = target.substr(colon_pos + 1); parse_generator_options(lang_args, [&](std::string k, std::string v) { if (k.find("include_prefix") != std::string::npos) { include_prefix = std::move(v); return CallbackLoopControl::Break; } return CallbackLoopControl::Continue; }); } // infer cpp include prefix from the filename passed in if none specified. if (include_prefix == "") { if (input_filename.rfind('/') != std::string::npos) { include_prefix = input_filename.substr(0, input_filename.rfind('/')); } } return include_prefix; } } // namespace std::unique_ptr<t_program_bundle> parse_and_get_program( const std::vector<std::string>& arguments) { // Parse arguments. parsing_params pparams{}; gen_params gparams{}; diagnostic_params dparams{}; std::string filename = parseArgs(arguments, pparams, gparams, dparams); if (filename.empty()) { return {}; } auto ctx = diagnostic_context::ignore_all(); return parse_and_mutate_program(ctx, filename, std::move(pparams)); } compile_result compile(const std::vector<std::string>& arguments) { compile_result result; // Parse arguments. g_stage = "arguments"; parsing_params pparams{}; gen_params gparams{}; diagnostic_params dparams{}; std::string input_filename = parseArgs(arguments, pparams, gparams, dparams); if (input_filename.empty()) { return result; } diagnostic_context ctx{result.detail, std::move(dparams)}; // Parse it! g_stage = "parse"; auto program = parse_and_mutate_program(ctx, input_filename, std::move(pparams)); if (!program || result.detail.has_failure()) { return result; } // TODO(afuller): Migrate to ast_mutator. try { mutator::mutate(program->root_program()); } catch (MutatorException& e) { ctx.report(std::move(e.message)); return result; } program->root_program()->set_include_prefix( get_include_path(gparams.targets, input_filename)); // Validate it! ctx.report_all(validator::validate(program->root_program())); standard_validator()(ctx, *program->root_program()); if (result.detail.has_failure()) { return result; } // Generate it! g_stage = "generation"; ctx.try_or_failure(*program->root_program(), [&]() { if (generate(gparams, program->root_program())) { result.retcode = compile_retcode::success; } }); return result; } } // namespace compiler } // namespace thrift } // namespace apache