mysqlshdk/shellcore/provider_polyglot.cc (268 lines of code) (raw):

/* * Copyright (c) 2024, Oracle and/or its affiliates. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, version 2.0, * as published by the Free Software Foundation. * * This program is designed to work with certain software (including * but not limited to OpenSSL) that is licensed under separate terms, * as designated in a particular file or component or in included license * documentation. The authors of MySQL hereby grant you an additional * permission to link the program and your derivative works with the * separately licensed software that they have either included with * the program or referenced in the documentation. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See * the GNU General Public License, version 2.0, for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "mysqlshdk/shellcore/provider_polyglot.h" #include <algorithm> #include <cctype> #include <iostream> #include <iterator> #include <set> #include <utility> #include "mysqlshdk/libs/utils/logger.h" #include "mysqlshdk/libs/utils/utils_lexing.h" #include "mysqlshdk/libs/utils/utils_string.h" #include "mysqlshdk/scripting/polyglot/polyglot_context.h" #include "mysqlshdk/scripting/polyglot/utils/polyglot_utils.h" namespace shcore { namespace completer { class Polyglot_proxy : public Object { public: explicit Polyglot_proxy(Provider_polyglot *completer, polyglot::Store obj, std::string obj_class) : m_completer(completer), m_object(std::move(obj)), m_object_class(std::move(obj_class)) {} std::string get_type() const override { return m_object_class; } bool is_member_callable(const std::string &name) const override { if (m_wrapped_placeholder) { return m_wrapped_placeholder->is_member_callable(name); } else { auto member = polyglot::get_member(m_object.thread(), m_object.get(), name); return polyglot::is_executable(m_object.thread(), member); } } std::shared_ptr<Object> get_member(const std::string &name) const override { std::shared_ptr<Object> member; bool callable; polyglot::Store object; std::string object_type; std::tie(callable, object, object_type) = m_completer->m_context->get_member_of(m_object.get(), name); if (!name.empty()) { if (callable || object_type.empty()) { // If the member is a method, we can't list their contents // to see the next completions, we need to list the contents of the // would be returned value, instead. // So instead of using a Proxy to the method, we // use a placeholder that knows the members the return type has m_wrapped_placeholder = m_completer->object_registry()->lookup(get_type()); } else { member = std::make_shared<Polyglot_proxy>( m_completer, std::move(object), std::move(object_type)); } } if (!member && m_wrapped_placeholder) { member = m_wrapped_placeholder->get_member(name); } return member; } size_t add_completions(const std::string &prefix, Completion_list *list) const override { size_t c = 0; // evaluate the string (which is expected to not have any side-effect) std::vector<std::pair<bool, std::string>> keys; keys = m_completer->m_context->get_members_of(m_object.get()); for (const auto &key : keys) { if (shcore::str_beginswith(key.second, prefix)) { list->push_back(key.second + (key.first ? "()" : "")); ++c; } } return c; } private: Provider_polyglot *m_completer; polyglot::Store m_object; std::string m_object_class; mutable std::shared_ptr<Object> m_wrapped_placeholder; }; Provider_polyglot::Provider_polyglot( std::shared_ptr<Object_registry> registry, std::shared_ptr<polyglot::Polyglot_context> context) : Provider_script(std::move(registry)), m_context(std::move(context)) {} Completion_list Provider_polyglot::complete_chain(const Chain &chain_a) { // handle globals/toplevel keywords Completion_list list; Chain chain(chain_a); if (chain.size() == 1) { std::vector<std::pair<bool, std::string>> globals( m_context->list_globals()); std::string prefix = chain.next().second; for (auto &i : globals) { if (shcore::str_beginswith(i.second, prefix)) { list.push_back(i.second + (i.first ? "()" : "")); } } for (auto &i : m_context->keywords()) { if (shcore::str_beginswith(i, prefix)) { list.push_back(i); } } } Completion_list more(Provider_script::complete_chain(chain)); std::copy(more.begin(), more.end(), std::back_inserter(list)); return list; } std::shared_ptr<Object> Provider_polyglot::lookup_global_object( const std::string &name) { polyglot::Store obj; std::string obj_type; std::tie(obj, obj_type) = m_context->get_global_polyglot(name); if (obj) { return std::make_shared<Polyglot_proxy>(this, std::move(obj), std::move(obj_type)); } return {}; } /** Parse the given string, and return the last Chain object in the string, considering a simplified scripting language syntax. completable:: identifier [whitespace*] ("." identifier | "." method_call)* method_call:: identifier "(" stuff ")" identifier:: ("_" | letters) ("_" | digits | letters)* stuff:: (anything | string | completable) stuff* */ Provider_script::Chain Provider_polyglot::parse_until(const std::string &s, size_t *pos, int close_char, size_t *chain_start_pos) { size_t end = s.length(); size_t &p = *pos; Provider_script::Chain chain; std::string identifier; while (p < end && !chain.invalid()) { switch (s[p]) { case '"': p = mysqlshdk::utils::span_quoted_string_dq(s, p); if (p == std::string::npos) { chain.invalidate(); return chain; } --p; break; case '\'': p = mysqlshdk::utils::span_quoted_string_sq(s, p); if (p == std::string::npos) { chain.invalidate(); return chain; } --p; break; case '{': case '[': case '(': { int closer = 0; switch (s[p]) { case '[': closer = ']'; break; case '{': closer = '}'; break; case '(': closer = ')'; break; } ++p; size_t inner_pos = p; Provider_script::Chain inner(parse_until(s, &p, closer, &inner_pos)); // if the inner block was not closed, then it's the last one if (p == end || inner.invalid()) { *chain_start_pos = inner_pos; return inner; } // otherwise, throw it away, consider it a method call/array/map // and continue if (!identifier.empty()) { if (closer == '}') { // this was a dict chain.clear(); } else if (closer == ']' && identifier.empty()) { // this was an array chain.clear(); } else if (closer == ')') { if (chain.add_method(identifier)) *chain_start_pos = p - identifier.length(); } else { if (chain.add_variable(identifier)) *chain_start_pos = p - identifier.length(); } identifier.clear(); } break; } case '}': case ')': case ']': if (s[p] == close_char) return chain; chain.clear(); identifier.clear(); // unexpected closing thingy, probably bad syntax, but we don't care break; case '\n': // a new line is a hard break on the previous part of a stmt case ' ': // or whitespace case '\t': case '\r': chain.clear(); identifier.clear(); break; case '/': // comment starting if (p + 1 < end) { if (s[p + 1] == '/') { // skip until EOL size_t nl = s.find('\n', p); if (nl == std::string::npos) { chain.clear(); identifier.clear(); p = end; } else { p = nl; } } else if (s[p + 1] == '*') { size_t eoc = mysqlshdk::utils::span_cstyle_comment(s, p); if (eoc == std::string::npos) { chain.invalidate(); return chain; } else { p = eoc; } } } break; case '\\': { // escape outside a string means line continuation.. // skip util end of line size_t nl = s.find('\n', p); if (nl == std::string::npos) { chain.clear(); identifier.clear(); p = end; } else { p = nl; } break; } case '.': if (!identifier.empty()) { if (chain.add_variable(identifier)) *chain_start_pos = p - identifier.length(); } else { // consecutive dots or dot at beginning = syntax error if (p == 0 || chain.empty() || s[p - 1] == '.') { chain.invalidate(); return chain; } } identifier.clear(); chain.add_dot(); break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': if (!identifier.empty()) { identifier.push_back(s[p]); } else { // a number can't appear in a chain, outside an id or param chain.clear(); } break; case '_': identifier.push_back(s[p]); break; default: // if we see any identifier-like char, we're in an identifier // anything else is a break if (isalpha(s[p])) { identifier.push_back(s[p]); } else { // garbage... chain.clear(); identifier.clear(); } break; } ++p; } if (chain.add_variable(identifier)) *chain_start_pos = p - identifier.length(); return chain; } } // namespace completer } // namespace shcore