Remove legacy lex-based tokenizer (`Scanner` object) from tab auto-complete logic...
[hiphop-php.git] / hphp / runtime / ext / vsdebug / completions_command.cpp
blobc26c29d32b32572d6403fe848322d6114d187ece
1 /*
2 +----------------------------------------------------------------------+
3 | HipHop for PHP |
4 +----------------------------------------------------------------------+
5 | Copyright (c) 2017-present Facebook, Inc. (http://www.facebook.com) |
6 +----------------------------------------------------------------------+
7 | This source file is subject to version 3.01 of the PHP license, |
8 | that is bundled with this package in the file LICENSE, and is |
9 | available through the world-wide-web at the following url: |
10 | http://www.php.net/license/3_01.txt |
11 | If you did not receive a copy of the PHP license and are unable to |
12 | obtain it through the world-wide-web, please send a note to |
13 | license@php.net so we can mail you a copy immediately. |
14 +----------------------------------------------------------------------+
17 #include "hphp/runtime/base/array-iterator.h"
18 #include "hphp/runtime/base/tv-variant.h"
19 #include "hphp/runtime/ext/vsdebug/command.h"
20 #include "hphp/runtime/ext/vsdebug/debugger.h"
21 #include "hphp/runtime/vm/named-entity-defs.h"
22 #include "hphp/runtime/vm/runtime-compiler.h"
23 #include "hphp/runtime/vm/vm-regs.h"
25 #include <algorithm>
27 namespace HPHP {
28 namespace VSDEBUG {
30 namespace {
31 class FramePointer {
32 public:
33 FramePointer(Debugger *debugger, int frameDepth)
34 : m_fp(nullptr),
35 m_exitDummyContext(false) {
36 VMRegAnchor _;
38 if (debugger->isDummyRequest()) {
39 m_fp = vmfp();
40 if (m_fp == nullptr && vmStack().count() == 0) {
41 g_context->enterDebuggerDummyEnv();
42 m_fp = vmfp();
43 m_exitDummyContext = true;
45 } else {
46 m_fp = g_context->getFrameAtDepthForDebuggerUnsafe(frameDepth);
50 ~FramePointer() {
51 if (m_exitDummyContext) {
52 g_context->exitDebuggerDummyEnv();
56 operator ActRec *() {
57 return m_fp;
60 ActRec * operator ->() {
61 return m_fp;
64 private:
65 ActRec *m_fp;
66 bool m_exitDummyContext;
70 CompletionsCommand::CompletionsCommand(
71 Debugger* debugger,
72 folly::dynamic message
73 ) : VSCommand(debugger, message),
74 m_frameId{0} {
76 const folly::dynamic& args = tryGetObject(message, "arguments", s_emptyArgs);
77 const int frameId = tryGetInt(args, "frameId", -1);
78 m_frameId = frameId;
81 CompletionsCommand::~CompletionsCommand() {
84 FrameObject* CompletionsCommand::getFrameObject(DebuggerSession* session) {
85 if (m_frameObj != nullptr) {
86 return m_frameObj;
89 m_frameObj = session->getFrameObject(m_frameId);
90 return m_frameObj;
93 request_id_t CompletionsCommand::targetThreadId(DebuggerSession* session) {
94 FrameObject* frame = getFrameObject(session);
95 if (frame == nullptr) {
96 // Execute the completion in the dummy context.
97 return Debugger::kDummyTheadId;
100 return frame->m_requestId;
103 void CompletionsCommand::addCompletionTarget(
104 folly::dynamic& completions,
105 const char* completionText,
106 const char* completionType,
107 int charsToOverwrite
109 completions.push_back(folly::dynamic::object);
110 folly::dynamic& completion = completions[completions.size() - 1];
111 completion["label"] = completionText;
112 completion["type"] = completionType;
114 // NOTE: "length" in the protocol is the number of chars to overwrite from
115 // the input text, counting backwards from "column" position in the
116 // CompletionsRequest message. Why this is called "length" is anybody's
117 // guess.
118 completion["length"] = charsToOverwrite;
121 CompletionsCommand::SuggestionContext
122 CompletionsCommand::parseCompletionExpression(
123 const std::string& expr
125 std::string text = expr;
126 SuggestionContext context;
127 context.type = SuggestionType::None;
128 return context;
131 bool CompletionsCommand::executeImpl(
132 DebuggerSession* session,
133 folly::dynamic* responseMsg
135 VMRegAnchor regAnchor;
137 // The request thread should not re-enter the debugger while
138 // processing this command.
139 DebuggerNoBreakContext noBreak(m_debugger);
141 folly::dynamic& message = getMessage();
142 const folly::dynamic& args = tryGetObject(message, "arguments", s_emptyArgs);
143 int column = tryGetInt(args, "column", -1);
144 std::string text = tryGetString(args, "text", "");
146 if (column == 0) {
147 // If no column specified, use the end of the string.
148 column = text.size() - 1;
149 } else {
150 // Completion column is 1-based, but std::string indexes are 0 based.
151 column--;
154 text = text.substr(0, column);
156 folly::dynamic body = folly::dynamic::object;
157 body["targets"] = folly::dynamic::array;
159 try {
160 SuggestionContext context = parseCompletionExpression(text);
161 auto& targets = body["targets"];
163 // If the target is not paused, completions are only valid for the dummy
164 // request thread.
165 if (!m_debugger->isPaused() &&
166 targetThreadId(session) != Debugger::kDummyTheadId) {
167 throw DebuggerCommandException("Target request is running.");
170 // Chop off any leading $.
171 if (context.matchPrefix[0] == '$') {
172 context.matchPrefix = context.matchPrefix.substr(1);
175 switch (context.type) {
176 case SuggestionType::None:
177 break;
179 case SuggestionType::Variable:
180 addVariableCompletions(context, targets);
181 break;
183 case SuggestionType::Member:
184 addMemberCompletions(context, targets);
185 break;
187 case SuggestionType::ClassStatic:
188 addClassStaticCompletions(context, targets);
189 break;
191 case SuggestionType::ClassConstant:
192 addClassConstantCompletions(context, targets);
193 break;
195 case SuggestionType::FuncsAndConsts:
196 addFuncConstantCompletions(context, targets);
197 break;
199 default:
200 assertx(false);
202 } catch (...) {
203 // Don't actually report any errors for completion requests, we just
204 // return an empty list if something goes wrong.
207 (*responseMsg)["body"] = body;
209 // Do not resume the target.
210 return false;
213 void CompletionsCommand::addVariableCompletions(
214 SuggestionContext& context,
215 folly::dynamic& targets
217 int frameDepth = m_frameObj ? m_frameObj->m_frameDepth : 0;
218 auto fp = FramePointer(m_debugger, frameDepth);
220 if (fp == nullptr) {
221 return;
224 // If there is a $this, add it.
225 if (
226 !fp->isInlined() &&
227 fp->func() != nullptr &&
228 fp->func()->cls() != nullptr &&
229 fp->hasThis()) {
230 static const std::string thisName("this");
231 if (context.matchPrefix.size() >= thisName.size() &&
232 std::equal(
233 thisName.begin(),
234 thisName.end(),
235 context.matchPrefix.begin())) {
237 addCompletionTarget(
238 targets,
239 thisName.c_str(),
240 CompletionTypeVar,
241 thisName.size()
246 // Add any defined variables that match the specified prefix.
247 const auto allVariables = getDefinedVariables(fp);
249 for (ArrayIter iter(allVariables); iter; ++iter) {
250 const std::string& name = iter.first().toString().toCppString();
251 addIfMatch(name, context.matchPrefix, CompletionTypeVar, targets);
255 void CompletionsCommand::addMemberCompletions(
256 SuggestionContext& context,
257 folly::dynamic& targets
259 int frameDepth = m_frameObj ? m_frameObj->m_frameDepth : 0;
260 FramePointer fp(m_debugger, frameDepth);
262 if (fp == nullptr) {
263 return;
266 // Don't execute the evaluation if the input string looks like it might
267 // contain a function call. This could have side effects we don't want to
268 // cause during auto complete suggestions and it's better to be safe here.
269 if (context.matchContext.find("(") != std::string::npos ||
270 context.matchContext.find(")") != std::string::npos ||
271 trimString(context.matchContext).size() == 0) {
273 return;
276 DebuggerRequestInfo* ri = m_debugger->getRequestInfo();
278 // SilentEvaluationContext suppresses all error output during the evaluation,
279 // and re-enables it when it is destroyed.
280 SilentEvaluationContext silentContext(m_debugger, ri);
282 // Figure out what object the context for the completion refers to. Don't
283 // hit any breakpoints during this eval.
284 // NOTE: Prefixing the evaluation with @ to suppress any PHP notices that
285 // might otherwise be generated by evaluating the expression.
286 context.matchContext = "<?hh return @(" + context.matchContext + ");";
287 std::unique_ptr<Unit> unit(
288 compile_debugger_string(context.matchContext.c_str(),
289 context.matchContext.size(),
290 g_context->getRepoOptionsForCurrentFrame()));
292 if (unit == nullptr) {
293 // Expression failed to compile.
294 return;
297 Unit* rawUnit = unit.get();
298 ri->m_evaluationUnits.push_back(std::move(unit));
299 const auto& result = g_context->evalPHPDebugger(
300 rawUnit,
301 frameDepth
304 if (result.failed) {
305 return;
308 const Variant& obj = result.result;
309 if (!obj.isObject()) {
310 return;
313 const auto object = obj.toObject().get();
314 if (object == nullptr) {
315 return;
318 // Add any matching instance properties of the object.
319 const Array instProps = object->toArray(false, true);
320 for (ArrayIter iter(instProps); iter; ++iter) {
321 std::string propName = iter.first().toString().toCppString();
323 // The object's property name can be encoded with info about the modifier
324 // and class. Decode it. (See HPHP::PreClass::manglePropName).
325 if (propName[1] == '*') {
326 // This is a protected property.
327 propName = propName.substr(3);
328 } else if (propName[0] == '\0') {
329 // This is a private property on this object class or one of its base
330 // classes.
331 const unsigned long classNameEnd = propName.find('\0', 1);
332 propName = propName.substr(classNameEnd + 1);
335 addIfMatch(propName, context.matchPrefix, CompletionTypeProp, targets);
338 // Add any instance methods of this object's class, or any of its parent
339 // classes. NB parent methods are automatic, no need to walk the tree.
340 Class* cls = object->getVMClass();
341 int methodCount = cls->numMethods();
342 for (Slot i = 0; i < methodCount; ++i) {
343 const Func* method = cls->getMethod(i);
344 if (method != nullptr && (method->attrs() & AttrStatic) == 0) {
345 const std::string& name = method->name()->toCppString();
346 addIfMatch(name, context.matchPrefix, CompletionTypeFn, targets);
351 void CompletionsCommand::addClassConstantCompletions(
352 SuggestionContext& context,
353 folly::dynamic& targets
355 HPHP::String classStr(context.matchContext.c_str());
356 Class* cls = Class::load(classStr.get());
357 if (cls == nullptr) {
358 return;
361 // Add constants of this class. Note that here and in methods, we get
362 // everything from this class and its ancestors
363 for (Slot i = 0; i < cls->numConstants(); i++) {
364 auto const &clsConst = cls->constants()[i];
365 // constants() includes type constants and abstract constants, neither of
366 // which are particularly useful for debugger completion
367 if (clsConst.kind() == ConstModifiers::Kind::Value
368 && !clsConst.isAbstractAndUninit()) {
369 const std::string& name = clsConst.name->toCppString();
370 addIfMatch(name, context.matchPrefix, CompletionTypeValue, targets);
374 // Add static methods of this class.
375 int methodCount = cls->numMethods();
376 for (Slot i = 0; i < methodCount; ++i) {
377 const Func* method = cls->getMethod(i);
378 if (method != nullptr && method->attrs() & AttrStatic) {
379 const std::string& name = method->name()->toCppString();
380 addIfMatch(name, context.matchPrefix, CompletionTypeFn, targets);
385 void CompletionsCommand::addClassStaticCompletions(
386 SuggestionContext& context,
387 folly::dynamic& targets
389 HPHP::String classStr(context.matchContext.c_str());
390 Class* cls = Class::load(classStr.get());
392 while (cls != nullptr) {
393 // Add static propreties of this class.
394 const auto staticProperties = cls->staticProperties();
395 for (Slot i = 0; i < cls->numStaticProperties(); i++) {
396 const auto prop = staticProperties[i];
397 const std::string& propName = prop.name.get()->toCppString();
398 addIfMatch(propName, context.matchPrefix, CompletionTypeProp, targets);
401 cls = cls->parent();
405 void CompletionsCommand::addIfMatch(
406 const std::string& name,
407 const std::string& matchPrefix,
408 const char* type,
409 folly::dynamic& targets
411 if (matchPrefix.empty() ||
412 (name.size() >= matchPrefix.size() &&
413 std::equal(
414 matchPrefix.begin(),
415 matchPrefix.end(),
416 name.begin(),
417 [](char c1, char c2) {
418 return std::toupper(c1) == std::toupper(c2);
420 ))) {
422 addCompletionTarget(
423 targets,
424 name.c_str(),
425 type,
426 matchPrefix.size()
431 void CompletionsCommand::addFuncConstantCompletions(
432 SuggestionContext& context,
433 folly::dynamic& targets
435 NamedEntity::foreach_cached_func([&](Func* func) {
436 if (func->isGenerated()) return; //continue
437 auto name = func->name()->toCppString();
438 // Unit::getFunctions returns all lowercase names, lowercase here too.
439 std::transform(name.begin(), name.end(), name.begin(), ::tolower);
440 addIfMatch(name, context.matchPrefix, CompletionTypeFn, targets);
443 auto const consts = lookupDefinedConstants();
444 IterateKV(consts.get(), [&] (TypedValue k, TypedValue) {
445 auto const& name = String::attach(tvCastToStringData(k));
446 addIfMatch(
447 name.toCppString(),
448 context.matchPrefix,
449 CompletionTypeFn,
450 targets
454 NamedEntity::foreach_cached_class([&](Class* c) {
455 if (!(c->attrs() & (AttrInterface | AttrTrait))) {
456 auto const& name = c->name()->toCppString();
457 addIfMatch(name, context.matchPrefix, CompletionTypeClass, targets);
461 // Add PHP keywords.
462 static const char* suggestionKeywords[] = {
463 "__CLASS__",
464 "__FILE__",
465 "__FUNCTION__",
466 "__LINE__",
467 "__METHOD__",
468 "abstract",
469 "array",
470 "as",
471 "bool",
472 "break",
473 "case",
474 "catch",
475 "class",
476 "clone",
477 "const",
478 "continue",
479 "default",
480 "do",
481 "echo",
482 "else",
483 "elseif",
484 "empty",
485 "endfor",
486 "endforeach",
487 "endif",
488 "endswitch",
489 "eval",
490 "exit",
491 "extends",
492 "final",
493 "for",
494 "foreach",
495 "function",
496 "if",
497 "implements",
498 "include",
499 "include_once",
500 "instanceof",
501 "int",
502 "interface",
503 "isset",
504 "list",
505 "new",
506 "null",
507 "parent",
508 "print",
509 "private",
510 "protected",
511 "public",
512 "require",
513 "require_once",
514 "return",
515 "self"
516 "static",
517 "string",
518 "switch",
519 "throw",
520 "try",
521 "unset",
522 "use",
523 "while"
526 const auto count = sizeof(suggestionKeywords) / sizeof(suggestionKeywords[0]);
528 for (int i = 0; i < count; i++) {
529 addIfMatch(
530 suggestionKeywords[i],
531 context.matchPrefix,
532 CompletionTypeKeyword,
533 targets