From d906bc3819d54a9d643459cf06de7ac69cf55e53 Mon Sep 17 00:00:00 2001 From: execjosh Date: Sun, 30 Dec 2012 01:23:10 +0900 Subject: [PATCH] Automate lazy generation of REPL completion lists All invokable methods, slots, signals, and properties visible from JavaScript, but which do not start with an underscore, are lazily (only when necessary) added to the completion list through dynamic reflection. This leverages `QMetaObject` for reflection of `QObject`s. As such, there is now no need to inherit `REPLCompletable` and it has been removed. http://code.google.com/p/phantomjs/issues/detail?id=943 --- src/filesystem.cpp | 52 +------------------------ src/filesystem.h | 12 +----- src/phantom.cpp | 23 +---------- src/phantom.h | 4 +- src/phantomjs.pro | 6 +-- src/repl.cpp | 51 ++++++++++++++++++++++++ src/repl.h | 3 ++ src/repl.js | 62 ++++++++++++++++++++++------- src/replcompletable.cpp | 86 ----------------------------------------- src/replcompletable.h | 60 ---------------------------- src/system.cpp | 15 +------ src/system.h | 4 +- src/webpage.cpp | 57 +-------------------------- src/webpage.h | 6 +-- src/webserver.cpp | 27 +------------ src/webserver.h | 11 +----- 16 files changed, 119 insertions(+), 360 deletions(-) delete mode 100644 src/replcompletable.cpp delete mode 100644 src/replcompletable.h diff --git a/src/filesystem.cpp b/src/filesystem.cpp index 0b4f932e..61c635fb 100644 --- a/src/filesystem.cpp +++ b/src/filesystem.cpp @@ -38,7 +38,7 @@ // File // public: File::File(QFile *openfile, QTextCodec *codec, QObject *parent) : - REPLCompletable(parent), + QObject(parent), m_file(openfile), m_fileStream(0) { @@ -213,23 +213,11 @@ void File::close() deleteLater(); } -void File::initCompletions() -{ - // Add completion for the Dynamic Properties of the 'file' object - // functions - addCompletion("read"); - addCompletion("write"); - addCompletion("readLine"); - addCompletion("writeLine"); - addCompletion("flush"); - addCompletion("close"); -} - // FileSystem // public: FileSystem::FileSystem(QObject *parent) - : REPLCompletable(parent) + : QObject(parent) { } // public slots: @@ -487,39 +475,3 @@ bool FileSystem::_remove(const QString &path) const bool FileSystem::_copy(const QString &source, const QString &destination) const { return QFile(source).copy(destination); } - -void FileSystem::initCompletions() -{ - // Add completion for the Dynamic Properties of the 'fs' object - // properties - addCompletion("separator"); - addCompletion("workingDirectory"); - // functions - addCompletion("list"); - addCompletion("absolute"); - addCompletion("readLink"); - addCompletion("exists"); - addCompletion("isDirectory"); - addCompletion("isFile"); - addCompletion("isAbsolute"); - addCompletion("isExecutable"); - addCompletion("isReadable"); - addCompletion("isWritable"); - addCompletion("isLink"); - addCompletion("changeWorkingDirectory"); - addCompletion("makeDirectory"); - addCompletion("makeTree"); - addCompletion("removeDirectory"); - addCompletion("removeTree"); - addCompletion("copyTree"); - addCompletion("open"); - addCompletion("read"); - addCompletion("write"); - addCompletion("size"); - addCompletion("remove"); - addCompletion("copy"); - addCompletion("move"); - addCompletion("touch"); - addCompletion("join"); - addCompletion("split"); -} diff --git a/src/filesystem.h b/src/filesystem.h index aa4ded10..7d5c22e6 100644 --- a/src/filesystem.h +++ b/src/filesystem.h @@ -36,9 +36,7 @@ #include #include -#include "replcompletable.h" - -class File : public REPLCompletable +class File : public QObject { Q_OBJECT @@ -66,16 +64,13 @@ public slots: void flush(); void close(); -private: - virtual void initCompletions(); - private: QFile *m_file; QTextStream *m_fileStream; }; -class FileSystem : public REPLCompletable +class FileSystem : public QObject { Q_OBJECT Q_PROPERTY(QString workingDirectory READ workingDirectory) @@ -139,9 +134,6 @@ public slots: bool isReadable(const QString &path) const; bool isWritable(const QString &path) const; bool isLink(const QString &path) const; - -private: - virtual void initCompletions(); }; #endif // FILESYSTEM_H diff --git a/src/phantom.cpp b/src/phantom.cpp index 21854ead..307dcbc4 100644 --- a/src/phantom.cpp +++ b/src/phantom.cpp @@ -54,7 +54,7 @@ static Phantom *phantomInstance = NULL; // private: Phantom::Phantom(QObject *parent) - : REPLCompletable(parent) + : QObject(parent) , m_terminated(false) , m_returnValue(0) , m_filesystem(0) @@ -471,24 +471,3 @@ void Phantom::doExit(int code) m_page = 0; QApplication::instance()->exit(code); } - -void Phantom::initCompletions() -{ - // Add completion for the Dynamic Properties of the 'phantom' object - // properties - addCompletion("args"); - addCompletion("defaultPageSettings"); - addCompletion("libraryPath"); - addCompletion("outputEncoding"); - addCompletion("scriptName"); - addCompletion("version"); - addCompletion("cookiesEnabled"); - addCompletion("cookies"); - // functions - addCompletion("exit"); - addCompletion("debugExit"); - addCompletion("injectJs"); - addCompletion("addCookie"); - addCompletion("deleteCookie"); - addCompletion("clearCookies"); -} diff --git a/src/phantom.h b/src/phantom.h index e1e88965..49bed7ff 100644 --- a/src/phantom.h +++ b/src/phantom.h @@ -37,7 +37,6 @@ #include "filesystem.h" #include "encoding.h" #include "config.h" -#include "replcompletable.h" #include "system.h" #include "childprocess.h" @@ -45,7 +44,7 @@ class WebPage; class CustomPage; class WebServer; -class Phantom: public REPLCompletable +class Phantom : public QObject { Q_OBJECT Q_PROPERTY(QStringList args READ args) @@ -181,7 +180,6 @@ private slots: private: void doExit(int code); - virtual void initCompletions(); Encoding m_scriptFileEnc; WebPage *m_page; diff --git a/src/phantomjs.pro b/src/phantomjs.pro index 92e1e7b4..e6f5de87 100644 --- a/src/phantomjs.pro +++ b/src/phantomjs.pro @@ -26,8 +26,7 @@ HEADERS += csconverter.h \ encoding.h \ config.h \ childprocess.h \ - repl.h \ - replcompletable.h + repl.h SOURCES += phantom.cpp \ callback.cpp \ @@ -45,8 +44,7 @@ SOURCES += phantom.cpp \ encoding.cpp \ config.cpp \ childprocess.cpp \ - repl.cpp \ - replcompletable.cpp + repl.cpp OTHER_FILES += \ bootstrap.js \ diff --git a/src/repl.cpp b/src/repl.cpp index e2321698..5ac69da2 100644 --- a/src/repl.cpp +++ b/src/repl.cpp @@ -33,6 +33,8 @@ #include #include #include +#include +#include #include "consts.h" #include "terminal.h" @@ -78,6 +80,52 @@ REPL *REPL::getInstance(QWebFrame *webframe, Phantom *parent) return singleton; } +QString REPL::_getClassName(QObject *obj) const +{ + const QMetaObject *meta = obj->metaObject(); + + return QString::fromLatin1(meta->className()); +} + +QStringList REPL::_enumerateCompletions(QObject *obj) const +{ + const QMetaObject *meta = obj->metaObject(); + QMap completions; + + // List up slots, signals, and invokable methods + const int methodOffset = meta->methodOffset(); + const int methodCount = meta->methodCount(); + for (int i = methodOffset; i < methodCount; i++) { + const QString name = QString::fromLatin1(meta->method(i).signature()); + // Ignore methods starting with underscores + if (name.startsWith('_')) { + continue; + } + // Keep only up to, but not including, first paren + const int cutoff = name.indexOf('('); + completions.insert((0 < cutoff ? name.left(cutoff) : name), true); + } + + // List up properties + const int propertyOffset = meta->propertyOffset(); + const int propertyCount = meta->propertyCount(); + for (int i = propertyOffset; i < propertyCount; i++) { + const QMetaProperty prop = meta->property(i); + // Ignore non-scriptable properties + if (!prop.isScriptable()) { + continue; + } + const QString name = QString::fromLatin1(prop.name()); + // Ignore properties starting with underscores + if (name.startsWith('_')) { + continue; + } + completions.insert(name, true); + } + + return completions.uniqueKeys(); +} + // private: REPL::REPL(QWebFrame *webframe, Phantom *parent) : QObject(parent), @@ -101,6 +149,9 @@ REPL::REPL(QWebFrame *webframe, Phantom *parent) // Inject REPL utility functions m_webframe->evaluateJavaScript(Utils::readResourceFileUtf8(":/repl.js"), QString("phantomjs://repl.js")); + // Add self to JavaScript world + m_webframe->addToJavaScriptWindowObject("_repl", this); + // Start the REPL's loop QTimer::singleShot(0, this, SLOT(startLoop())); } diff --git a/src/repl.h b/src/repl.h index dfd1582b..dbea5957 100644 --- a/src/repl.h +++ b/src/repl.h @@ -58,6 +58,9 @@ public: static bool instanceExists(); static REPL *getInstance(QWebFrame *webframe = NULL, Phantom *parent = NULL); + Q_INVOKABLE QString _getClassName(QObject *obj) const; + Q_INVOKABLE QStringList _enumerateCompletions(QObject *obj) const; + private: REPL(QWebFrame *webframe, Phantom *parent); static void offerCompletion(const char *buf, linenoiseCompletions *lc); diff --git a/src/repl.js b/src/repl.js index aca03996..ee92b06f 100644 --- a/src/repl.js +++ b/src/repl.js @@ -31,6 +31,13 @@ var REPL = REPL || {}; +(function () { + +/** + * Cache to hold completions + */ +var _cache = {}; + /** * Return the Completions of the Object, applying the prefix * @@ -40,16 +47,45 @@ var REPL = REPL || {}; REPL._getCompletions = function (obj, prefix) { var completions = []; - // If the given object is "(REPL)Completable", just return it's completions - if (obj._isCompletable && obj._isCompletable() === true) { - completions = obj._getCompletions(prefix || ""); + if (typeof prefix !== "string") { + prefix = ""; } else { - // It's a JS Native Object: build the list of completions manually - for (k in obj) { - if (obj.hasOwnProperty(k) && k.indexOf(prefix || "") === 0) { - completions.push(k); + prefix = prefix.trim(); + } + + try { + // Try to get `QObject` inherited class's name (throws exception if not + // inherited from `QObject`) + var className = _repl._getClassName(obj); + + // Initialize completions for this class as needed + if (null == _cache[className]) { + _cache[className] = _repl._enumerateCompletions(obj); + } + + var key = className; + if ("" !== prefix) { + key = "-" + prefix; + if (null == _cache[key]) { + // Filter out completions + var regexp = new RegExp("^" + prefix); + _cache[key] = _cache[className].filter(function (elm) { + return regexp.test(elm); + }); } } + completions = _cache[key]; + } catch (e) { + try { + Object.keys(obj).forEach(function (k) { + if (obj.hasOwnProperty(k) && k.indexOf(prefix) === 0) { + completions.push(k); + } + }); + completions.sort(); + } catch (e) { + // Ignore... + } } return completions; @@ -67,14 +103,12 @@ REPL._expResStringifyReplacer = function (k, v) { mock = {}, funcToStr = "[Function]"; - // If the result of the last evaluated expression is a REPLCompletable object + // If the result of the last evaluated expression is an object if (k === "" //< only first level of recursive calls - && REPL._lastEval - && REPL._lastEval._isCompletable - && REPL._lastEval._isCompletable() === true) { + && REPL._lastEval) { - // Get all the completions for the REPLCompletable object we are going to pretty-print - iarr = REPL._lastEval._getCompletions(""); + // Get all the completions for the object we are going to pretty-print + iarr = REPL._getCompletions(REPL._lastEval); for (i in iarr) { if (typeof(v[iarr[i]]) !== "undefined") { // add a reference to this "real" property into the mock object @@ -96,3 +130,5 @@ REPL._expResStringifyReplacer = function (k, v) { return v; }; + +})(); diff --git a/src/replcompletable.cpp b/src/replcompletable.cpp deleted file mode 100644 index 0216e437..00000000 --- a/src/replcompletable.cpp +++ /dev/null @@ -1,86 +0,0 @@ -#include "replcompletable.h" - -// public: -REPLCompletable::REPLCompletable(QObject *parent) - : QObject(parent), - mCompletionsInitialised(false) -{ } - -REPLCompletable::~REPLCompletable() -{ } - -bool REPLCompletable::_isCompletable() -{ - return true; -} - -QStringList REPLCompletable::_getCompletions(const QString &prefix) -{ - // First time this method is invoked, initialise the completions (lazy init) - if (!mCompletionsInitialised) { - initCompletions(); - mCompletionsInitialised = true; - } - - // If no prefix provided? - if (prefix.isEmpty()) { - // Return all the possible completions - return REPLCompletable::getCompletionsIndex()->values( - this->metaObject()->className()); - } - - // make a key to store the (new) completions list - QString cacheKey = QString("%1-%2").arg(this->metaObject()->className()).arg(prefix); - // If a list of completion withi this key is not already in the cache - if (!getCompletionsCache()->contains(cacheKey)) { - // Loop over the completions and pick the one that match the given prefix - QStringList allCompletions = REPLCompletable::getCompletionsIndex()->values( - this->metaObject()->className()); - QStringList *matchingPrefixCompletions = new QStringList(); - - QStringList::iterator i; - for (i = allCompletions.begin(); i != allCompletions.end(); ++i) { - if (((QString) *i).startsWith(prefix)) { - matchingPrefixCompletions->append((QString) *i); - } - } - - // Store the result in the cache - getCompletionsCache()->insert(cacheKey, matchingPrefixCompletions); - } - - return *(getCompletionsCache()->object(cacheKey)); -} - -// protected: -void REPLCompletable::addCompletion(const char *completion) -{ - addCompletion(QString(completion)); -} - -void REPLCompletable::addCompletion(QString completion) -{ - // Accept a completion only if it's unique per MetaObject ClassName - if (!REPLCompletable::getCompletionsIndex()->contains(this->metaObject()->className(), completion)) { - REPLCompletable::getCompletionsIndex()->insert(this->metaObject()->className(), completion); - } -} - -// private: -QMultiHash *REPLCompletable::getCompletionsIndex() -{ - static QMultiHash *compIndex = NULL; - if (!compIndex) { - compIndex = new QMultiHash(); - } - return compIndex; -} - -QCache *REPLCompletable::getCompletionsCache() -{ - static QCache *compCache = NULL; - if (!compCache) { - compCache = new QCache(); - } - return compCache; -} diff --git a/src/replcompletable.h b/src/replcompletable.h deleted file mode 100644 index ecd8f313..00000000 --- a/src/replcompletable.h +++ /dev/null @@ -1,60 +0,0 @@ -#ifndef REPLCOMPLETABLE_H -#define REPLCOMPLETABLE_H - -#include -#include -#include - -/** - * This subclass of QObject is used by the REPL to better control - * what is "shown" of a QObject exposed in the Javascript Runtime. - * - * By default the JS environment will see all the slots and the Q_INVOKABLE - * of a "exposed" QObject. But also some extra QObject specific methods - * that, in our case, we prefer not to list during REPL autocompletion - * listing or expression result prettyfication. - */ -class REPLCompletable : public QObject -{ - Q_OBJECT - -public: - REPLCompletable(QObject *parent = 0); - virtual ~REPLCompletable(); - - Q_INVOKABLE bool _isCompletable(); - Q_INVOKABLE QStringList _getCompletions(const QString &prefixToComplete); - -protected: - /** - * Used by sublcasses to register a possible "completion". - * - * @param completion Array of characters representing a function/property - * that will be listed as possible completion - */ - void addCompletion(const char *completion); - /** - * Used by sublcasses to register a possible "completion". - * - * @param completion String representing a function/property - * that will be listed as possible completion - */ - void addCompletion(QString completion); - -private: - /** - * This is where subclasses should use REPLCompletable#addCompletion(...) - * to declare/register their completions for the REPL. - * This ensures that ONLY if a REPL is actually requested by the user, - * we bother registering the completion strings. - */ - virtual void initCompletions() = 0; - - static QMultiHash *getCompletionsIndex(); - static QCache *getCompletionsCache(); - -private: - bool mCompletionsInitialised; -}; - -#endif // REPLCOMPLETABLE_H diff --git a/src/system.cpp b/src/system.cpp index 263d8e6d..738542dd 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -38,7 +38,7 @@ #include "../env.h" System::System(QObject *parent) : - REPLCompletable(parent) + QObject(parent) , m_stdout((File *)NULL) , m_stderr((File *)NULL) , m_stdin((File *)NULL) @@ -201,16 +201,3 @@ QObject *System::_stdin() { return m_stdin; } - -void System::initCompletions() -{ - addCompletion("pid"); - addCompletion("args"); - addCompletion("env"); - addCompletion("platform"); - addCompletion("os"); - addCompletion("isSSLSupported"); - addCompletion("stdin"); - addCompletion("stdout"); - addCompletion("stderr"); -} diff --git a/src/system.h b/src/system.h index 46f78d83..677ac752 100644 --- a/src/system.h +++ b/src/system.h @@ -37,11 +37,10 @@ #include #include "filesystem.h" -#include "replcompletable.h" // This class implements the CommonJS System/1.0 spec. // See: http://wiki.commonjs.org/wiki/System/1.0 -class System : public REPLCompletable +class System : public QObject { Q_OBJECT Q_PROPERTY(qint64 pid READ pid) @@ -81,7 +80,6 @@ private: QStringList m_args; QVariant m_env; QMap m_os; - virtual void initCompletions(); File *m_stdout; File *m_stderr; File *m_stdin; diff --git a/src/webpage.cpp b/src/webpage.cpp index 7dd3f4d2..33a61272 100644 --- a/src/webpage.cpp +++ b/src/webpage.cpp @@ -295,7 +295,7 @@ private: WebPage::WebPage(QObject *parent, const QUrl &baseUrl) - : REPLCompletable(parent) + : QObject(parent) , m_navigationLocked(false) , m_mousePos(QPoint(0, 0)) , m_ownsPages(true) @@ -1451,59 +1451,4 @@ void WebPage::setupFrame(QWebFrame *frame) injectCallbacksObjIntoFrame(frame == NULL ? m_mainFrame : frame, m_callbacks); } -void WebPage::initCompletions() -{ - // Add completion for the Dynamic Properties of the 'webpage' object - // properties - addCompletion("clipRect"); - addCompletion("content"); - addCompletion("libraryPath"); - addCompletion("settings"); - addCompletion("viewportSize"); - addCompletion("ownsPages"); - addCompletion("windowName"); - addCompletion("pages"); - addCompletion("pagesWindowName"); - addCompletion("frameName"); - addCompletion("framesName"); - addCompletion("framesCount"); - addCompletion("cookies"); - // functions - addCompletion("evaluate"); - addCompletion("includeJs"); - addCompletion("injectJs"); - addCompletion("open"); - addCompletion("release"); - addCompletion("render"); - addCompletion("renderBase64"); - addCompletion("sendEvent"); - addCompletion("uploadFile"); - addCompletion("getPage"); - addCompletion("switchToFrame"); - addCompletion("switchToMainFrame"); - addCompletion("switchToParentFrame"); - addCompletion("switchToFocusedFrame"); - addCompletion("addCookie"); - addCompletion("deleteCookie"); - addCompletion("clearCookies"); - addCompletion("setContent"); - // callbacks - addCompletion("onAlert"); - addCompletion("onCallback"); - addCompletion("onPrompt"); - addCompletion("onConfirm"); - addCompletion("onFilePicker"); - addCompletion("onConsoleMessage"); - addCompletion("onInitialized"); - addCompletion("onLoadStarted"); - addCompletion("onLoadFinished"); - addCompletion("onResourceRequested"); - addCompletion("onResourceReceived"); - addCompletion("onUrlChanged"); - addCompletion("onNavigationRequested"); - addCompletion("onError"); - addCompletion("onPageCreated"); - addCompletion("onClosing"); -} - #include "webpage.moc" diff --git a/src/webpage.h b/src/webpage.h index 927408e6..657bc061 100644 --- a/src/webpage.h +++ b/src/webpage.h @@ -36,8 +36,6 @@ #include #include -#include "replcompletable.h" - class Config; class CustomPage; class WebpageCallbacks; @@ -45,7 +43,7 @@ class NetworkAccessManager; class QWebInspector; class Phantom; -class WebPage: public REPLCompletable, public QWebFrame::PrintCallback +class WebPage : public QObject, public QWebFrame::PrintCallback { Q_OBJECT Q_PROPERTY(QString title READ title) @@ -492,8 +490,6 @@ private: bool javaScriptConfirm(const QString &msg); bool javaScriptPrompt(const QString &msg, const QString &defaultValue, QString *result); - virtual void initCompletions(); - private: CustomPage *m_customWebPage; NetworkAccessManager *m_networkAccessManager; diff --git a/src/webserver.cpp b/src/webserver.cpp index d9f10818..9c603543 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -93,7 +93,7 @@ static void *callback(mg_event event, } WebServer::WebServer(QObject *parent) - : REPLCompletable(parent) + : QObject(parent) , m_ctx(0) { setObjectName("WebServer"); @@ -268,23 +268,11 @@ bool WebServer::handleRequest(mg_event event, mg_connection *conn, const mg_requ return true; } -void WebServer::initCompletions() -{ - // Add completion for the Dynamic Properties of the 'webpage' object - // properties - addCompletion("clipRect"); - // functions - addCompletion("listen"); - addCompletion("close"); - // callbacks - addCompletion("onNewRequest"); -} - //BEGIN WebServerResponse WebServerResponse::WebServerResponse(mg_connection* conn, QSemaphore* close) - : REPLCompletable() + : QObject() , m_conn(conn) , m_statusCode(200) , m_headersSent(false) @@ -472,15 +460,4 @@ void WebServerResponse::setHeaders(const QVariantMap &headers) m_headers = headers; } -void WebServerResponse::initCompletions() -{ - // Add completion for the Dynamic Properties of the 'webpage' object - // properties - addCompletion("statusCode"); - addCompletion("headers"); - // functions - addCompletion("writeHead"); - addCompletion("write"); -} - //END WebServerResponse diff --git a/src/webserver.h b/src/webserver.h index 9a4a9f92..1cbb5642 100644 --- a/src/webserver.h +++ b/src/webserver.h @@ -36,7 +36,6 @@ #include #include "mongoose.h" -#include "replcompletable.h" class Config; @@ -47,7 +46,7 @@ class WebServerResponse; * * see also: modules/webserver.js */ -class WebServer : public REPLCompletable +class WebServer : public QObject { Q_OBJECT Q_PROPERTY(QString port READ port) @@ -84,9 +83,6 @@ signals: public: bool handleRequest(mg_event event, mg_connection *conn, const mg_request_info *request); -private: - virtual void initCompletions(); - private: mg_context *m_ctx; QString m_port; @@ -99,7 +95,7 @@ private: /** * Outgoing HTTP response to client. */ -class WebServerResponse : public REPLCompletable +class WebServerResponse : public QObject { Q_OBJECT @@ -146,9 +142,6 @@ public slots: /// set all headers void setHeaders(const QVariantMap &headers); -private: - virtual void initCompletions(); - private: mg_connection *m_conn; int m_statusCode;