[gjs/wip/coverage-2: 2/9] Add GjsReflectedScript



commit 560138a7c39f000d3b16e43a6a7929eeb5a2f1bd
Author: Sam Spilsbury <smspillaz gmail com>
Date:   Wed Jan 1 15:25:07 2014 -0800

    Add GjsReflectedScript
    
    GjsReflectedScript contains some useful information about a
    script, including an array with all available function names,
    array of all branches and an array of all executable lines.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=721246

 Makefile-insttest.am                  |    1 +
 Makefile-test.am                      |    3 +-
 Makefile.am                           |    3 +
 gjs/reflected-script-private.h        |   56 +++
 gjs/reflected-script.cpp              |  764 +++++++++++++++++++++++++++++++++
 gjs/reflected-script.h                |   93 ++++
 installed-tests/js/testInfoReflect.js |  528 +++++++++++++++++++++++
 modules/infoReflect.js                |  338 +++++++++++++++
 modules/modules.gresource.xml.in      |    1 +
 test/gjs-test-reflected-script.cpp    |  419 ++++++++++++++++++
 test/gjs-tests-add-funcs.h            |   25 ++
 test/gjs-tests.cpp                    |    4 +
 12 files changed, 2234 insertions(+), 1 deletions(-)
---
diff --git a/Makefile-insttest.am b/Makefile-insttest.am
index c1ea941..f736537 100644
--- a/Makefile-insttest.am
+++ b/Makefile-insttest.am
@@ -109,6 +109,7 @@ dist_jstests_DATA += \
        installed-tests/js/testGObjectClass.js          \
        installed-tests/js/testJS1_8.js                 \
        installed-tests/js/testLang.js                  \
+       installed-tests/js/testInfoReflect.js           \
        installed-tests/js/testLocale.js                        \
        installed-tests/js/testMainloop.js                      \
        installed-tests/js/testMetaClass.js                     \
diff --git a/Makefile-test.am b/Makefile-test.am
index 27b2204..895ebfc 100644
--- a/Makefile-test.am
+++ b/Makefile-test.am
@@ -22,7 +22,8 @@ gjs_tests_LDADD =             \
        $(GJSTESTS_LIBS)
 
 gjs_tests_SOURCES =            \
-       test/gjs-tests.cpp
+       test/gjs-tests.cpp \
+       test/gjs-test-reflected-script.cpp
 
 check-local: gjs-tests
        @test -z "${TEST_PROGS}" || ${GTESTER} --verbose ${TEST_PROGS} ${TEST_PROGS_OPTIONS}
diff --git a/Makefile.am b/Makefile.am
index 5059966..3028ead 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -36,6 +36,7 @@ nobase_gjs_module_include_HEADERS =   \
        gjs/type-module.h       \
        gjs/mem.h               \
        gjs/native.h    \
+       gjs/reflected-script.h \
        gi/ns.h         \
        gi/object.h     \
        gi/foreign.h    \
@@ -55,6 +56,7 @@ nobase_gjs_module_include_HEADERS =   \
 
 noinst_HEADERS +=              \
        gjs/jsapi-private.h     \
+       gjs/reflected-script-private.h \
        gi/proxyutils.h         \
        util/crash.h            \
        util/hash-x32.h         \
@@ -106,6 +108,7 @@ libgjs_la_SOURCES =         \
        gjs/importer.cpp                \
        gjs/gi.h                \
        gjs/gi.cpp              \
+       gjs/reflected-script.cpp \
        gjs/jsapi-private.cpp   \
        gjs/jsapi-util.cpp      \
        gjs/jsapi-dynamic-class.cpp \
diff --git a/gjs/reflected-script-private.h b/gjs/reflected-script-private.h
new file mode 100644
index 0000000..444beb2
--- /dev/null
+++ b/gjs/reflected-script-private.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright © 2014 Endless Mobile, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Authored By: Sam Spilsbury <sam endlessm com>
+ */
+#ifndef GJS_REFLECTED_SCRIPT_PRIVATE_H
+#define GJS_REFLECTED_SCRIPT_PRIVATE_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+typedef struct _GjsReflectedScriptBranchInfo {
+    unsigned int branch_point;
+    GArray       *branch_alternatives;
+} GjsReflectedScriptBranchInfo;
+
+GjsReflectedScriptBranchInfo *
+gjs_reflected_script_branch_info_new(unsigned int branch_point,
+                                     GArray       *alternatives);
+
+void
+gjs_reflected_script_branch_info_destroy(gpointer info);
+
+typedef struct _GjsReflectedScriptFunctionInfo {
+    unsigned int n_params;
+    unsigned int line_number;
+    char         *name;
+} GjsReflectedScriptFunctionInfo;
+
+/* This function takes ownership of the string */
+GjsReflectedScriptFunctionInfo *
+gjs_reflected_script_function_info_new(char         *name,
+                                       unsigned int line_number,
+                                       unsigned int n_params);
+
+void
+gjs_reflected_script_function_info_destroy(gpointer info);
+
+G_END_DECLS
+
+#endif
diff --git a/gjs/reflected-script.cpp b/gjs/reflected-script.cpp
new file mode 100644
index 0000000..336145c
--- /dev/null
+++ b/gjs/reflected-script.cpp
@@ -0,0 +1,764 @@
+/*
+ * Copyright © 2014 Endless Mobile, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Authored By: Sam Spilsbury <sam endlessm com>
+ */
+#include <glib-object.h>
+
+#include <gio/gio.h>
+
+#include <gjs/gjs.h>
+#include <gjs/jsapi-util.h>
+#include <gjs/compat.h>
+#include <gjs/gjs-module.h>
+
+#include <gjs/reflected-script.h>
+#include "reflected-script-private.h"
+
+struct _GjsReflectedScriptPrivate {
+    /* External context where the reflection happens. We take
+     * a reference to it so that we can still do evaluation even
+     * if the original owner of the context goes away
+     *
+     * One important precondition is that the context needs to
+     * be "initialized" to a state that we expect it to be in,
+     * eg, we need to have loaded the infoReflect.js script
+     * and set up some global variables. If it hasn't then
+     * this will trigger an assertion */
+    GjsContext *reflection_context;
+
+    char *script_filename;
+
+    /* Array of strings, null-terminated */
+    GArray *all_function_names;
+
+    /* Array of GjsReflectedScriptBranchInfo */
+    GArray *all_branches;
+
+    /* Sorted array of unsigned int */
+    GArray *all_expression_lines;
+
+    /* Number of lines */
+    unsigned int n_lines;
+
+    /* A flag which indicates whether or not reflection
+     * data has been gathered for this script yet. Reflect.parse
+     * can be a super-expensive operation for large scripts so
+     * we should perform it on-demand when we actually need to */
+    gboolean reflection_performed;
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE(GjsReflectedScript,
+                           gjs_reflected_script,
+                           G_TYPE_OBJECT)
+
+enum {
+    PROP_0,
+    PROP_SCRIPT_FILENAME,
+    PROP_REFLECTION_CONTEXT,
+    PROP_N
+};
+
+static GParamSpec *properties[PROP_N];
+
+static void ensure_script_reflected(GjsReflectedScript *script);
+
+GjsReflectedScriptBranchInfo *
+gjs_reflected_script_branch_info_new(unsigned int  branch_point,
+                                     GArray       *alternatives)
+{
+    GjsReflectedScriptBranchInfo *info = g_new0(GjsReflectedScriptBranchInfo, 1);
+    info->branch_point = branch_point;
+    info->branch_alternatives = alternatives;
+    return info;
+}
+
+void
+gjs_reflected_script_branch_info_destroy(gpointer info_data)
+{
+    GjsReflectedScriptBranchInfo *info = (GjsReflectedScriptBranchInfo *) info_data;
+    g_array_free(info->branch_alternatives, TRUE);
+    g_free(info);
+}
+
+unsigned int
+gjs_reflected_script_branch_info_get_branch_point(const GjsReflectedScriptBranchInfo *info)
+{
+    return info->branch_point;
+}
+
+const unsigned int *
+gjs_reflected_script_branch_info_get_branch_alternatives(const GjsReflectedScriptBranchInfo *info,
+                                                         unsigned int                       *n)
+{
+    g_return_val_if_fail(n, NULL);
+
+    *n = info->branch_alternatives->len;
+    return (unsigned int *) info->branch_alternatives->data;
+}
+
+GjsReflectedScriptFunctionInfo *
+gjs_reflected_script_function_info_new(char         *name,
+                                       unsigned int  line_number,
+                                       unsigned int  n_params)
+{
+    GjsReflectedScriptFunctionInfo *info = (GjsReflectedScriptFunctionInfo *) 
g_new0(GjsReflectedScriptFunctionInfo, 1);
+    info->name = name;
+    info->line_number = line_number;
+    info->n_params = n_params;
+
+    return info;
+}
+
+unsigned int
+gjs_reflected_script_function_info_get_line_number(const GjsReflectedScriptFunctionInfo *info)
+{
+    return info->line_number;
+}
+
+unsigned int
+gjs_reflected_script_function_info_get_n_params(const GjsReflectedScriptFunctionInfo *info)
+{
+    return info->n_params;
+}
+
+const char *
+gjs_reflected_script_function_info_get_name(const GjsReflectedScriptFunctionInfo *info)
+{
+    return info->name;
+}
+
+void
+gjs_reflected_script_function_info_destroy(gpointer info_data)
+{
+    GjsReflectedScriptFunctionInfo *info = (GjsReflectedScriptFunctionInfo *) info_data;
+
+    if (info->name)
+        g_free(info->name);
+
+    g_free(info);
+}
+
+
+const GjsReflectedScriptBranchInfo **
+gjs_reflected_script_get_branches(GjsReflectedScript *script)
+{
+    g_return_val_if_fail(script, NULL);
+
+    GjsReflectedScriptPrivate *priv = (GjsReflectedScriptPrivate *) 
gjs_reflected_script_get_instance_private(script);
+
+    ensure_script_reflected(script);
+
+    g_assert(priv->all_branches);
+    return (const GjsReflectedScriptBranchInfo **) priv->all_branches->data;\
+}
+
+const GjsReflectedScriptFunctionInfo **
+gjs_reflected_script_get_functions(GjsReflectedScript *script)
+{
+    g_return_val_if_fail(script, NULL);
+
+    GjsReflectedScriptPrivate *priv = (GjsReflectedScriptPrivate *) 
gjs_reflected_script_get_instance_private(script);
+
+    ensure_script_reflected(script);
+
+    g_assert(priv->all_function_names);
+    return (const GjsReflectedScriptFunctionInfo **) priv->all_function_names->data;
+}
+
+const unsigned int *
+gjs_reflected_script_get_expression_lines(GjsReflectedScript *script,
+                                          unsigned int       *n)
+{
+    g_return_val_if_fail(script, NULL);
+    g_return_val_if_fail(n, NULL);
+
+    g_return_val_if_fail(n, NULL);
+
+    GjsReflectedScriptPrivate *priv = (GjsReflectedScriptPrivate *) 
gjs_reflected_script_get_instance_private(script);
+
+    ensure_script_reflected(script);
+
+    g_assert(priv->all_expression_lines);
+    if (priv->all_expression_lines) {
+        *n = priv->all_expression_lines->len;
+        return (const unsigned int *) priv->all_expression_lines->data;
+    }
+}
+
+typedef gboolean (*ConvertAndInsertJSVal) (GArray    *array,
+                                           JSContext *context,
+                                           jsval     *element);
+
+static gboolean
+get_array_from_js_value(JSContext             *context,
+                        jsval                 *value,
+                        size_t                 array_element_size,
+                        GDestroyNotify         element_clear_func,
+                        ConvertAndInsertJSVal  inserter,
+                        GArray                **out_array)
+{
+    g_return_val_if_fail(out_array != NULL, FALSE);
+    g_return_val_if_fail(*out_array == NULL, FALSE);
+
+    JSObject *js_array = JSVAL_TO_OBJECT(*value);
+
+    if (!JS_IsArrayObject(context, js_array)) {
+        g_critical("Returned object from is not an array");
+        return FALSE;
+    }
+
+    /* We're not preallocating any space here at the moment until
+     * we have some profiling data that suggests a good size to
+     * preallocate to.
+     *
+     * TODO: Preallocating would be nice. */
+    GArray *script_functions_array = g_array_new(TRUE, TRUE, array_element_size);
+    u_int32_t script_funtions_array_len;
+
+    if (element_clear_func)
+        g_array_set_clear_func(script_functions_array, element_clear_func);
+
+    if (JS_GetArrayLength(context, js_array, &script_funtions_array_len)) {
+        u_int32_t i = 0;
+        for (; i < script_funtions_array_len; ++i) {
+            jsval element;
+            if (!JS_GetElement(context, js_array, i, &element)) {
+                g_array_unref(script_functions_array);
+                gjs_throw(context, "Failed to get function names array element %i", i);
+                return FALSE;
+            }
+
+            if (!(inserter(script_functions_array, context, &element))) {
+                g_array_unref(script_functions_array);
+                gjs_throw(context, "Failed to convert array element %i", i);
+                return FALSE;
+            }
+        }
+    }
+
+    *out_array = script_functions_array;
+
+    return TRUE;
+}
+
+static GArray *
+call_js_function_for_array_return(JSContext             *context,
+                                  JSObject              *object,
+                                  size_t                 array_element_size,
+                                  GDestroyNotify         element_clear_func,
+                                  ConvertAndInsertJSVal  inserter,
+                                  const char            *function_name,
+                                  jsval                 *ast)
+{
+    GArray *array = NULL;
+    jsval rval;
+    if (!JS_CallFunctionName(context, object, function_name, 1, ast, &rval)) {
+        gjs_log_exception(context);
+        return NULL;
+    }
+
+    if (!get_array_from_js_value(context,
+                                 &rval,
+                                 array_element_size,
+                                 element_clear_func,
+                                 inserter,
+                                 &array)) {
+        gjs_log_exception(context);
+        return NULL;
+    }
+
+    return array;
+}
+
+static void
+clear_reflected_script_function_info(gpointer info_location)
+{
+    GjsReflectedScriptFunctionInfo **info_ptr = (GjsReflectedScriptFunctionInfo **) info_location;
+
+    gjs_reflected_script_function_info_destroy(*info_ptr);
+}
+
+static gboolean
+convert_and_insert_function_decl(GArray    *array,
+                                 JSContext *context,
+                                 jsval     *element)
+{
+    JSObject *object = JSVAL_TO_OBJECT(*element);
+
+    if (!object) {
+        gjs_throw(context, "Converting element to object failed");
+        return FALSE;
+    }
+
+    jsval line_number_property_value;
+    if (!JS_GetProperty(context, object, "line", &line_number_property_value) ||
+        !JSVAL_IS_INT(line_number_property_value)) {
+        gjs_throw(context, "Failed to get line property for function object");
+        return FALSE;
+    }
+
+    unsigned int line_number = JSVAL_TO_INT(line_number_property_value);
+
+    jsval n_params_property_value;
+    if (!JS_GetProperty(context, object, "n_params", &n_params_property_value) ||
+        !JSVAL_IS_INT(n_params_property_value)) {
+        gjs_throw(context, "Failed to get n_params property for function object");
+        return FALSE;
+    }
+
+    unsigned int n_params = JSVAL_TO_INT(n_params_property_value);
+
+    jsval    function_name_property_value;
+
+    if (!JS_GetProperty(context, object, "name", &function_name_property_value)) {
+        gjs_throw(context, "Failed to get name property for function object");
+        return FALSE;
+    }
+
+    char *utf8_string;
+
+    if (JSVAL_IS_STRING(function_name_property_value)) {
+        if (!gjs_string_to_utf8(context,
+                                function_name_property_value,
+                                &utf8_string)) {
+            gjs_throw(context, "Failed to convert function_name to string");
+            return FALSE;
+        }
+    } else if (JSVAL_IS_NULL(function_name_property_value)) {
+        utf8_string = NULL;
+    } else {
+        gjs_throw(context, "Unexpected type for function_name");
+        return FALSE;
+    }
+
+
+    GjsReflectedScriptFunctionInfo *info =
+        gjs_reflected_script_function_info_new(utf8_string,
+                                               line_number,
+                                               n_params);
+
+    g_array_append_val(array, info);
+
+    return TRUE;
+}
+
+static GArray *
+get_script_functions_from_reflection(JSContext *context,
+                                     JSObject  *global,
+                                     jsval     *ast)
+{
+    return call_js_function_for_array_return(context,
+                                             global,
+                                             sizeof(GjsReflectedScriptFunctionInfo *),
+                                             clear_reflected_script_function_info,
+                                             convert_and_insert_function_decl,
+                                             "functionsForAST",
+                                             ast);
+}
+
+static gboolean
+convert_and_insert_unsigned_int(GArray    *array,
+                                JSContext *context,
+                                jsval     *element)
+{
+    if (!JSVAL_IS_INT(*element)) {
+        g_critical("Array element is not an integer");
+        return FALSE;
+    }
+
+    unsigned int element_integer = JSVAL_TO_INT(*element);
+    g_array_append_val(array, element_integer);
+    return TRUE;
+}
+
+static int
+uint_compare(gconstpointer left,
+             gconstpointer right)
+{
+    unsigned int *left_int = (unsigned int *) left;
+    unsigned int *right_int = (unsigned int *) right;
+
+    return *left_int - *right_int;
+}
+
+static GArray *
+get_all_lines_with_executable_expressions_from_reflection(JSContext *context,
+                                                          JSObject  *global,
+                                                          jsval     *ast)
+{
+    GArray *all_expressions = call_js_function_for_array_return(context,
+                                                                global,
+                                                                sizeof(unsigned int),
+                                                                NULL,
+                                                                convert_and_insert_unsigned_int,
+                                                                "expressionLinesForAST",
+                                                                ast);
+
+    /* Sort, just to be sure */
+    g_array_sort(all_expressions, uint_compare);
+    return all_expressions;
+}
+
+static void
+gjs_reflected_script_branch_info_clear(gpointer branch_info_location)
+{
+    GjsReflectedScriptBranchInfo **info_ptr = (GjsReflectedScriptBranchInfo **) branch_info_location;
+    gjs_reflected_script_branch_info_destroy(*info_ptr);
+}
+
+static gboolean
+convert_and_insert_branch_info(GArray    *array,
+                               JSContext *context,
+                               jsval     *element)
+{
+    if (!JSVAL_IS_OBJECT(*element)) {
+        gjs_throw(context, "Array element is not an object");
+        return FALSE;
+    }
+
+    JSObject *object = JSVAL_TO_OBJECT(*element);
+
+    if (!object) {
+        gjs_throw(context, "Converting element to object failed");
+        return FALSE;
+    }
+
+    jsval   branch_point_value;
+    int32_t branch_point;
+
+    if (!JS_GetProperty(context, object, "point", &branch_point_value) ||
+        !JSVAL_IS_INT(branch_point_value)) {
+        gjs_throw(context, "Failed to get point property from element");
+        return FALSE;
+    }
+
+    branch_point = JSVAL_TO_INT(branch_point_value);
+
+    jsval  branch_exits_value;
+    GArray *branch_exists_array = NULL;
+
+    if (!JS_GetProperty(context, object, "exits", &branch_exits_value) ||
+        !JSVAL_IS_OBJECT(branch_exits_value)) {
+        gjs_throw(context, "Failed to get exits property from element");
+        return FALSE;
+    }
+
+    if (!get_array_from_js_value(context,
+                                 &branch_exits_value,
+                                 sizeof(unsigned int),
+                                 NULL,
+                                 convert_and_insert_unsigned_int,
+                                 &branch_exists_array)) {
+        /* Already logged the exception, no need to do anything here */
+        return FALSE;
+    }
+
+    GjsReflectedScriptBranchInfo *info = gjs_reflected_script_branch_info_new(branch_point,
+                                                                              branch_exists_array);
+
+    g_array_append_val(array, info);
+
+    return TRUE;
+}
+
+static GArray *
+get_script_branches_from_reflection(JSContext *context,
+                                    JSObject  *global,
+                                    jsval     *ast)
+{
+    return call_js_function_for_array_return(context,
+                                             global,
+                                             sizeof(GjsReflectedScriptBranchInfo *),
+                                             gjs_reflected_script_branch_info_clear,
+                                             convert_and_insert_branch_info,
+                                             "branchesForAST",
+                                             ast);
+}
+
+static unsigned int
+count_lines_in_script(const char *data)
+{
+    int lines = 1;
+    for (; *data; ++data)
+        if (*data == '\n')
+            ++lines;
+    return lines;
+}
+
+static JSString *
+load_script_for_reflection(GjsContext   *context,
+                           const char   *filename,
+                           int          *start_line_number,
+                           unsigned int *script_n_lines)
+{
+    JSContext *js_context = (JSContext *) gjs_context_get_native_context(context);
+    GFile     *script_file = g_file_new_for_commandline_arg(filename);
+
+    if (!g_file_query_exists(script_file, NULL)) {
+        g_object_unref(script_file);
+        gjs_throw(js_context, "Script: %s does not exist!", filename);
+        return NULL;
+    }
+
+    char  *original_script_contents;
+    gsize script_contents_len;
+    if (!g_file_load_contents(script_file,
+                              NULL,
+                              &original_script_contents,
+                              &script_contents_len,
+                              NULL,
+                              NULL)) {
+        g_object_unref(script_file);
+        gjs_throw(js_context, "Failed to get script contents for %s", filename);
+        return FALSE;
+    }
+
+    /* Number of lines in the script must be based on the original script contents
+     * as we get line numbers relative to the starting line there */
+    *script_n_lines = count_lines_in_script(original_script_contents);
+
+    /* gjs_script_unix_shebang will modify start_line_number if necessary */
+    const char *modified_script_contents =
+        gjs_strip_unix_shebang(original_script_contents,
+                               (gssize *) &script_contents_len,
+                               start_line_number);
+
+    JSString *str = JS_NewStringCopyZ(js_context, modified_script_contents);
+
+    g_free(original_script_contents);
+    g_object_unref(script_file);
+
+    return str;
+}
+
+static gboolean
+perform_reflection_within_compartment(GjsContext         *internal_context,
+                                      GjsReflectedScript *script)
+{
+    GjsReflectedScriptPrivate *priv = (GjsReflectedScriptPrivate *) 
gjs_reflected_script_get_instance_private(script);
+    JSContext *js_context = (JSContext *) gjs_context_get_native_context(internal_context);
+    JSObject *global = JS_GetGlobalObject(js_context);
+
+    JSAutoCompartment ac(js_context, global);
+
+    int          start_line_number = 1;
+    unsigned int script_n_lines;
+
+    JSString *str = load_script_for_reflection(internal_context,
+                                               priv->script_filename,
+                                               &start_line_number,
+                                               &script_n_lines);
+
+    if (!str)
+        return FALSE;
+
+    jsval reflect_object_value;
+    if (!JS_GetProperty(js_context, global, "Reflect", &reflect_object_value) ||
+        !JSVAL_IS_OBJECT(reflect_object_value)) {
+        gjs_throw(js_context, "'Reflect' object not found in context");
+        return FALSE;
+    }
+
+    JSObject *reflect_object = JSVAL_TO_OBJECT(reflect_object_value);
+    JSObject *reflect_options_object = JS_NewObject(js_context, NULL, NULL, NULL);
+
+    jsval loc_value = BOOLEAN_TO_JSVAL(JS_TRUE);
+    jsval line_value = INT_TO_JSVAL(start_line_number);
+
+    JS_SetProperty(js_context, reflect_options_object, "loc", &loc_value);
+    JS_SetProperty(js_context, reflect_options_object, "line", &line_value);
+
+    jsval parseArgv[] = {
+        STRING_TO_JSVAL(str),
+        OBJECT_TO_JSVAL(reflect_options_object)
+    };
+    jsval ast_value;
+
+    if (!JS_CallFunctionName(js_context, reflect_object, "parse", 2, parseArgv, &ast_value)) {
+        gjs_throw(js_context, "Failed to call Reflect.parse");
+        return FALSE;
+    }
+
+    priv->all_function_names = get_script_functions_from_reflection(js_context,
+                                                                    global,
+                                                                    &ast_value);
+    priv->all_branches = get_script_branches_from_reflection(js_context,
+                                                             global,
+                                                             &ast_value);
+    priv->all_expression_lines = get_all_lines_with_executable_expressions_from_reflection(js_context,
+                                                                                           global,
+                                                                                           &ast_value);
+    priv->n_lines = script_n_lines;
+
+    return TRUE;
+}
+
+static void
+ensure_script_reflected(GjsReflectedScript *script)
+{
+    GjsReflectedScriptPrivate *priv = (GjsReflectedScriptPrivate *) 
gjs_reflected_script_get_instance_private(script);
+
+    if (priv->reflection_performed)
+        return;
+
+    if (!perform_reflection_within_compartment(priv->reflection_context, script)) {
+        g_warning("Reflecting script %s failed", priv->script_filename);
+        /* If the reflection failed, we should make sure that the the reflection
+         * details have sane defaults */
+        priv->all_branches = g_array_new(TRUE, TRUE, sizeof(GjsReflectedScriptBranchInfo));
+        priv->all_function_names = g_array_new(TRUE, TRUE, sizeof(GjsReflectedScriptFunctionInfo));
+        priv->all_expression_lines = g_array_new(TRUE, TRUE, sizeof(unsigned int));
+        priv->n_lines = 0;
+    }
+
+    priv->reflection_performed = TRUE;
+}
+
+GjsContext *
+gjs_reflected_script_create_reflection_context()
+{
+    static const char *resource_path = "resource:///org/gnome/gjs/modules/infoReflect.js";
+
+    GjsContext *context = gjs_context_new();
+
+    if (!gjs_context_eval_file(context,
+                               resource_path,
+                               NULL,
+                               NULL)) {
+        g_object_unref(context);
+        return NULL;
+    }
+
+    gjs_context_pop();
+
+    return context;
+}
+
+unsigned int
+gjs_reflected_script_get_n_lines(GjsReflectedScript *script)
+{
+    g_return_val_if_fail(script, 0);
+
+    GjsReflectedScriptPrivate *priv = (GjsReflectedScriptPrivate *) 
gjs_reflected_script_get_instance_private(script);
+
+    ensure_script_reflected(script);
+
+    return priv->n_lines;
+}
+
+static void
+gjs_reflected_script_init(GjsReflectedScript *script)
+{
+    GjsReflectedScriptPrivate *priv = (GjsReflectedScriptPrivate *) 
gjs_reflected_script_get_instance_private(script);
+
+    priv->all_branches = NULL;
+    priv->all_function_names = NULL;
+    priv->all_expression_lines = NULL;
+}
+
+static void
+unref_array_if_nonnull(GArray *array)
+{
+    if (array)
+        g_array_unref(array);
+}
+
+static void
+gjs_reflected_script_dispose(GObject *object)
+{
+    GjsReflectedScript *script = GJS_REFLECTED_SCRIPT(object);
+    GjsReflectedScriptPrivate *priv = (GjsReflectedScriptPrivate *) 
gjs_reflected_script_get_instance_private(script);
+
+    g_clear_object(&priv->reflection_context);
+
+    G_OBJECT_CLASS(gjs_reflected_script_parent_class)->dispose(object);
+}
+
+static void
+gjs_reflected_script_finalize(GObject *object)
+{
+    GjsReflectedScript *script = GJS_REFLECTED_SCRIPT(object);
+    GjsReflectedScriptPrivate *priv = (GjsReflectedScriptPrivate *) 
gjs_reflected_script_get_instance_private(script);
+
+    unref_array_if_nonnull(priv->all_branches);
+    unref_array_if_nonnull(priv->all_function_names);
+    unref_array_if_nonnull(priv->all_expression_lines);
+
+    g_free(priv->script_filename);
+
+    G_OBJECT_CLASS(gjs_reflected_script_parent_class)->finalize(object);
+}
+
+static void
+gjs_reflected_script_set_property(GObject      *object,
+                                  unsigned int  prop_id,
+                                  const GValue *value,
+                                  GParamSpec   *pspec)
+{
+    GjsReflectedScript *script = GJS_REFLECTED_SCRIPT(object);
+    GjsReflectedScriptPrivate *priv = (GjsReflectedScriptPrivate *) 
gjs_reflected_script_get_instance_private(script);
+
+    switch (prop_id) {
+    case PROP_SCRIPT_FILENAME:
+        priv->script_filename = g_value_dup_string(value);
+        break;
+    case PROP_REFLECTION_CONTEXT:
+        priv->reflection_context = (GjsContext *) g_value_dup_object(value);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+    }
+}
+
+static void
+gjs_reflected_script_class_init(GjsReflectedScriptClass *klass)
+{
+    GObjectClass *object_class = G_OBJECT_CLASS(klass);
+
+    object_class->set_property = gjs_reflected_script_set_property;
+    object_class->dispose = gjs_reflected_script_dispose;
+    object_class->finalize = gjs_reflected_script_finalize;
+
+    properties[PROP_0] = NULL;
+    properties[PROP_SCRIPT_FILENAME] = g_param_spec_string("filename",
+                                                           "Script Filename",
+                                                           "Valid path to script",
+                                                           NULL,
+                                                           (GParamFlags) (G_PARAM_CONSTRUCT_ONLY | 
G_PARAM_WRITABLE));
+    properties[PROP_REFLECTION_CONTEXT] = g_param_spec_object("reflection-context",
+                                                              "Reflection Context",
+                                                              "Context to perform reflection in. "
+                                                              "This must have been initialized with 
infoReflect.js loaded ",
+                                                              GJS_TYPE_CONTEXT,
+                                                              (GParamFlags) (G_PARAM_CONSTRUCT_ONLY | 
G_PARAM_WRITABLE));
+    g_object_class_install_properties(object_class,
+                                      PROP_N,
+                                      properties);
+}
+
+GjsReflectedScript *
+gjs_reflected_script_new(const char *filename,
+                         GjsContext *reflection_context)
+{
+    GjsReflectedScript *script =
+        GJS_REFLECTED_SCRIPT(g_object_new(GJS_TYPE_REFLECTED_SCRIPT,
+                                          "filename", filename,
+                                          "reflection-context", reflection_context,
+                                          NULL));
+    return script;
+}
diff --git a/gjs/reflected-script.h b/gjs/reflected-script.h
new file mode 100644
index 0000000..9e2ea17
--- /dev/null
+++ b/gjs/reflected-script.h
@@ -0,0 +1,93 @@
+/*
+ * Copyright © 20134Endless Mobile, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Authored By: Sam Spilsbury <sam endlessm com>
+ */
+#ifndef GJS_REFLECTED_SCRIPT_H
+#define GJS_REFLECTED_SCRIPT_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GJS_TYPE_REFLECTED_SCRIPT gjs_reflected_script_get_type()
+
+#define GJS_REFLECTED_SCRIPT(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), \
+     GJS_TYPE_REFLECTED_SCRIPT, GjsReflectedScript))
+
+#define GJS_REFLECTED_SCRIPT_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), \
+     GJS_TYPE_REFLECTED_SCRIPT, GjsReflectedScriptClass))
+
+#define GJS_IS_REFLECTED_SCRIPT(obj) \
+    (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \
+     GJS_TYPE_REFLECTED_SCRIPT))
+
+#define GJS_IS_REFLECTED_SCRIPT_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_TYPE ((klass), \
+     GJS_TYPE_REFLECTED_SCRIPT))
+
+#define GJS_REFLECTED_SCRIPT_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS ((obj), \
+     GJS_TYPE_REFLECTED_SCRIPT, GjsReflectedScriptClass))
+
+typedef struct _GjsReflectedScript GjsReflectedScript;
+typedef struct _GjsReflectedScriptClass GjsReflectedScriptClass;
+typedef struct _GjsReflectedScriptPrivate GjsReflectedScriptPrivate;
+
+typedef struct _GjsReflectedScriptBranchInfo GjsReflectedScriptBranchInfo;
+typedef struct _GjsReflectedScriptFunctionInfo GjsReflectedScriptFunctionInfo;
+
+typedef struct _GjsContext GjsContext;
+
+struct _GjsReflectedScriptClass {
+    GObjectClass parent_class;
+};
+
+struct _GjsReflectedScript {
+    GObject parent;
+};
+
+unsigned int gjs_reflected_script_branch_info_get_branch_point(const GjsReflectedScriptBranchInfo *info);
+const unsigned int * gjs_reflected_script_branch_info_get_branch_alternatives(const 
GjsReflectedScriptBranchInfo *info,
+                                                                              unsigned int                   
    *n);
+
+unsigned int gjs_reflected_script_function_info_get_line_number(const GjsReflectedScriptFunctionInfo *info);
+unsigned int gjs_reflected_script_function_info_get_n_params(const GjsReflectedScriptFunctionInfo *info);
+const char * gjs_reflected_script_function_info_get_name(const GjsReflectedScriptFunctionInfo *info);
+
+const GjsReflectedScriptFunctionInfo ** gjs_reflected_script_get_functions(GjsReflectedScript *script);
+const unsigned int * gjs_reflected_script_get_expression_lines(GjsReflectedScript *script,
+                                                               unsigned int       *n_executable_lines);
+const GjsReflectedScriptBranchInfo ** gjs_reflected_script_get_branches(GjsReflectedScript *script);
+unsigned int gjs_reflected_script_get_n_lines(GjsReflectedScript *script);
+
+GType gjs_reflected_script_get_type(void);
+
+GjsReflectedScript * gjs_reflected_script_new(const char *filename,
+                                              GjsContext *reflection_context);
+
+/* Creates a "reflection context" that can be passed to the constructor of
+ * gjs_reflected_script_new. This context will have the script
+ * containing the functions which permit reflection pre-defined and can
+ * be shared across all reflections */
+GjsContext * gjs_reflected_script_create_reflection_context();
+
+G_END_DECLS
+
+#endif
diff --git a/installed-tests/js/testInfoReflect.js b/installed-tests/js/testInfoReflect.js
new file mode 100644
index 0000000..9deceda
--- /dev/null
+++ b/installed-tests/js/testInfoReflect.js
@@ -0,0 +1,528 @@
+const JSUnit = imports.jsUnit;
+const InfoReflect = imports.infoReflect;
+
+function parseScriptForExpressionLines(script) {
+    const ast = Reflect.parse(script);
+    return InfoReflect.expressionLinesForAST(ast);
+}
+
+function assertArrayEquals(actual, expected, assertion) {
+    if (actual.length != expected.length)
+        throw "Arrays not equal length. Actual array was " +
+                actual.length + " and Expected array was " +
+                expected.length;
+
+    for (let i = 0; i < actual.length; i++) {
+        assertion(expected[i], actual[i]);
+    }
+}
+
+function testExpressionLinesFoundForAssignmentExpressionSides() {
+    let foundLinesOnBothExpressionSides =
+        parseScriptForExpressionLines("var x;\n" +
+                                      "x = (function() {\n" +
+                                      "    return 10;\n" +
+                                      "})();\n");
+    assertArrayEquals(foundLinesOnBothExpressionSides,
+                      [1, 2, 3],
+                      JSUnit.assertEquals);
+}
+
+
+function testExpressionLinesFoundForLinesInsideFunctions() {
+    let foundLinesInsideNamedFunction =
+        parseScriptForExpressionLines("function f(a, b) {\n" +
+                                      "    let x = a;\n" +
+                                      "    let y = b;\n" +
+                                      "    return x + y;\n" +
+                                      "}\n" +
+                                      "\n" +
+                                      "var z = f(1, 2);\n");
+    assertArrayEquals(foundLinesInsideNamedFunction,
+                      [2, 3, 4, 7],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForLinesInsideAnonymousFunctions() {
+    let foundLinesInsideAnonymousFunction =
+        parseScriptForExpressionLines("var z = (function f(a, b) {\n" +
+                                      "     let x = a;\n" +
+                                      "     let y = b;\n" +
+                                      "     return x + y;\n" +
+                                      " })();\n");
+    assertArrayEquals(foundLinesInsideAnonymousFunction,
+                      [1, 2, 3, 4],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForBodyOfFunctionProperty() {
+    let foundLinesInsideFunctionProperty =
+        parseScriptForExpressionLines("var o = {\n" +
+                                      "    foo: function () {\n" +
+                                      "        let x = a;\n" +
+                                      "    }\n" +
+                                      "};\n");
+    assertArrayEquals(foundLinesInsideFunctionProperty,
+                      [1, 2, 3],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForCallArgsOfFunctionProperty() {
+    let foundLinesInsideCallArgs =
+        parseScriptForExpressionLines("function f(a) {\n" +
+                                      "}\n" +
+                                      "f({\n" +
+                                      "    foo: function() {\n" +
+                                      "        let x = a;\n" +
+                                      "    }\n" +
+                                      "});\n");
+    assertArrayEquals(foundLinesInsideCallArgs,
+                      [1, 3, 4, 5],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForMultilineCallArgs() {
+    let foundLinesInsideMultilineCallArgs =
+        parseScriptForExpressionLines("function f(a, b, c) {\n" +
+                                      "}\n" +
+                                      "f(1,\n" +
+                                      "  2,\n" +
+                                      "  3);\n");
+    assertArrayEquals(foundLinesInsideMultilineCallArgs,
+                      [1, 3, 4, 5],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForNewCallWithObject() {
+    let foundLinesInsideObjectCallArg =
+        parseScriptForExpressionLines("function f(o) {\n" +
+                                      "}\n" +
+                                      "let obj = {\n" +
+                                      "    Name: new f({ a: 1,\n" +
+                                      "                  b: 2,\n" +
+                                      "                  c: 3\n" +
+                                      "                })\n" +
+                                      "}\n");
+    assertArrayEquals(foundLinesInsideObjectCallArg,
+                      [1, 3, 4, 5, 6],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForWhileLoop() {
+    let foundLinesInsideWhileLoop =
+        parseScriptForExpressionLines("var a = 0;\n" +
+                                      "while (a < 1) {\n" +
+                                      "    let x = 0;\n" +
+                                      "    let y = 1;\n" +
+                                      "    a++;" +
+                                      "\n" +
+                                      "}\n");
+    assertArrayEquals(foundLinesInsideWhileLoop,
+                      [1, 2, 3, 4, 5],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForTryCatchFinally() {
+    let foundLinesInsideTryCatchFinally =
+        parseScriptForExpressionLines("var a = 0;\n" +
+                                      "try {\n" +
+                                      "    a++;\n" +
+                                      "} catch (e) {\n" +
+                                      "    a++;\n" +
+                                      "} finally {\n" +
+                                      "    a++;\n" +
+                                      "}\n");
+    assertArrayEquals(foundLinesInsideTryCatchFinally,
+                      [1, 2, 3, 4, 5, 7],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForCaseStatements() {
+    let foundLinesInsideCaseStatements =
+        parseScriptForExpressionLines("var a = 0;\n" +
+                                      "switch (a) {\n" +
+                                      "case 1:\n" +
+                                      "    a++;\n" +
+                                      "    break;\n" +
+                                      "case 2:\n" +
+                                      "    a++;\n" +
+                                      "    break;\n" +
+                                      "}\n");
+    assertArrayEquals(foundLinesInsideCaseStatements,
+                      [1, 2, 4, 5, 7, 8],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForLoop() {
+    let foundLinesInsideLoop =
+        parseScriptForExpressionLines("for (let i = 0; i < 1; i++) {\n" +
+                                      "    let x = 0;\n" +
+                                      "    let y = 1;\n" +
+                                      "\n" +
+                                      "}\n");
+    assertArrayEquals(foundLinesInsideLoop,
+                      [1, 2, 3],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForIfExits() {
+    let foundLinesInsideIfExits =
+        parseScriptForExpressionLines("if (1 > 0) {\n" +
+                                      "    let i = 0;\n" +
+                                      "} else {\n" +
+                                      "    let j = 1;\n" +
+                                      "}\n");
+    assertArrayEquals(foundLinesInsideIfExits,
+                      [1, 2, 4],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForFirstLineOfMultilineIfTests() {
+    let foundLinesInsideMultilineIfTest =
+        parseScriptForExpressionLines("if (1 > 0 &&\n" +
+                                      "    2 > 0 &&\n" +
+                                      "    3 > 0){\n" +
+                                      "    let a = 3;\n" +
+                                      "}\n");
+    assertArrayEquals(foundLinesInsideMultilineIfTest,
+                      [1, 4],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForObjectPropertyLiterals() {
+    let foundLinesInsideObjectPropertyLiterals =
+        parseScriptForExpressionLines("var a = {\n" +
+                                      "    Name: 'foo',\n" +
+                                      "    Ex: 'bar'\n" +
+                                      "};\n");
+    assertArrayEquals(foundLinesInsideObjectPropertyLiterals,
+                      [1, 2, 3],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForObjectPropertyFunction() {
+    let foundLinesInsideObjectPropertyFunction =
+        parseScriptForExpressionLines("var a = {\n" +
+                                      "    Name: function() {},\n" +
+                                      "};\n");
+    assertArrayEquals(foundLinesInsideObjectPropertyFunction,
+                      [1, 2],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForObjectPropertyFunction() {
+    let foundLinesInsideObjectPropertyFunction =
+        parseScriptForExpressionLines("var a = {\n" +
+                                      "    Name: function() {},\n" +
+                                      "};\n");
+    assertArrayEquals(foundLinesInsideObjectPropertyFunction,
+                      [1, 2],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForObjectPropertyObjectExpression() {
+    let foundLinesInsideObjectPropertyObjectExpression =
+        parseScriptForExpressionLines("var a = {\n" +
+                                      "    Name: {},\n" +
+                                      "};\n");
+    assertArrayEquals(foundLinesInsideObjectPropertyObjectExpression,
+                      [1, 2],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForObjectPropertyArrayExpression() {
+    let foundLinesInsideObjectPropertyObjectExpression =
+        parseScriptForExpressionLines("var a = {\n" +
+                                      "    Name: [],\n" +
+                                      "};\n");
+    assertArrayEquals(foundLinesInsideObjectPropertyObjectExpression,
+                      [1, 2],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForObjectArgsToReturn() {
+    let foundLinesInsideObjectArgsToReturn =
+        parseScriptForExpressionLines("var a = {\n" +
+                                      "    Name: {},\n" +
+                                      "};\n");
+    assertArrayEquals(foundLinesInsideObjectArgsToReturn,
+                      [1, 2],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForObjectArgsToThrow() {
+    let foundLinesInsideObjectArgsToThrow =
+        parseScriptForExpressionLines("function f() {\n" +
+                                      "    throw {\n" +
+                                      "        a: 1,\n" +
+                                      "        b: 2\n" +
+                                      "    }\n" +
+                                      "}\n");
+    assertArrayEquals(foundLinesInsideObjectArgsToThrow,
+                      [2, 3, 4],
+                      JSUnit.assertEquals);
+}
+
+
+function parseScriptForFunctionNames(script) {
+    const ast = Reflect.parse(script);
+    return InfoReflect.functionsForAST(ast);
+}
+
+function functionDeclarationsEqual(actual, expected) {
+    JSUnit.assertEquals(expected.name, actual.name);
+    JSUnit.assertEquals(expected.line, actual.line);
+    JSUnit.assertEquals(expected.n_params, actual.n_params);
+}
+
+function testFunctionsFoundForDeclarations() {
+    let foundFunctionDeclarations =
+        parseScriptForFunctionNames("function f1() {}\n" +
+                                    "function f2() {}\n" +
+                                    "function f3() {}\n");
+    assertArrayEquals(foundFunctionDeclarations,
+                      [
+                          { name: "f1", line: 1, n_params: 0 },
+                          { name: "f2", line: 2, n_params: 0 },
+                          { name: "f3", line: 3, n_params: 0 }
+                      ],
+                      functionDeclarationsEqual);
+}
+
+function testFunctionsFoundForNestedFunctions() {
+    let foundFunctions =
+        parseScriptForFunctionNames("function f1() {\n" +
+                                    "    let f2 = function() {\n" +
+                                    "        let f3 = function() {\n" +
+                                    "        }\n" +
+                                    "    }\n" +
+                                    "}\n");
+    assertArrayEquals(foundFunctions,
+                      [
+                          { name: "f1", line: 1, n_params: 0 },
+                          { name: null, line: 2, n_params: 0 },
+                          { name: null, line: 3, n_params: 0 }
+                      ],
+                      functionDeclarationsEqual);
+}
+
+function testFunctionsFoundOnSameLineButDifferentiatedOnArgs() {
+    /* Note the lack of newlines. This is all on
+     * one line */
+    let foundFunctionsOnSameLine =
+        parseScriptForFunctionNames("function f1() {" +
+                                    "    return (function (a) {" +
+                                    "        return function (a, b) {}" +
+                                    "    });" +
+                                    "}");
+    assertArrayEquals(foundFunctionsOnSameLine,
+                      [
+                          { name: "f1", line: 1, n_params: 0 },
+                          { name: null, line: 1, n_params: 1 },
+                          { name: null, line: 1, n_params: 2 }
+                      ],
+                      functionDeclarationsEqual);
+}
+
+function parseScriptForBranches(script) {
+    const ast = Reflect.parse(script);
+    return InfoReflect.branchesForAST(ast);
+}
+
+function branchInfoEqual(actual, expected) {
+    JSUnit.assertEquals(expected.point, actual.point);
+    assertArrayEquals(expected.exits, actual.exits, JSUnit.assertEquals);
+}
+
+function testBothBranchExitsFoundForSimpleBranch() {
+    let foundBranchExitsForSimpleBranch =
+        parseScriptForBranches("if (1) {\n" +
+                               "    let a = 1;\n" +
+                               "} else {\n" +
+                               "    let b = 2;\n" +
+                               "}\n");
+    assertArrayEquals(foundBranchExitsForSimpleBranch,
+                      [
+                          { point: 1, exits: [2, 4] }
+                      ],
+                      branchInfoEqual);
+}
+
+function testSingleExitFoundForBranchWithOneConsequent() {
+    let foundBranchExitsForSingleConsequentBranch =
+        parseScriptForBranches("if (1) {\n" +
+                               "    let a = 1.0;\n" +
+                               "}\n");
+    assertArrayEquals(foundBranchExitsForSingleConsequentBranch,
+                      [
+                          { point: 1, exits: [2] }
+                      ],
+                      branchInfoEqual);
+}
+
+function testMultipleBranchesFoundForNestedIfElseBranches() {
+    let foundBranchesForNestedIfElseBranches =
+        parseScriptForBranches("if (1) {\n" +
+                               "    let a = 1.0;\n" +
+                               "} else if (2) {\n" +
+                               "    let b = 2.0;\n" +
+                               "} else if (3) {\n" +
+                               "    let c = 3.0;\n" +
+                               "} else {\n" +
+                               "    let d = 4.0;\n" +
+                               "}\n");
+    assertArrayEquals(foundBranchesForNestedIfElseBranches,
+                      [
+                          /* the 'else if' line is actually an
+                           * exit for the first branch */
+                          { point: 1, exits: [2, 3] },
+                          { point: 3, exits: [4, 5] },
+                          /* 'else' by itself is not executable,
+                           * it is the block it contains whcih
+                           * is */
+                          { point: 5, exits: [6, 8] }
+                      ],
+                      branchInfoEqual);
+}
+
+
+function testSimpleTwoExitBranchWithoutBlocks() {
+    let foundBranches =
+        parseScriptForBranches("let a, b;\n" +
+                               "if (1)\n" +
+                               "    a = 1.0\n" +
+                               "else\n" +
+                               "    b = 2.0\n" +
+                               "\n");
+    assertArrayEquals(foundBranches,
+                      [
+                          { point: 2, exits: [3, 5] }
+                      ],
+                      branchInfoEqual);
+}
+
+function testNoBranchFoundIfConsequentWasEmpty() {
+    let foundBranches =
+        parseScriptForBranches("let a, b;\n" +
+                               "if (1);\n");
+    assertArrayEquals(foundBranches,
+                      [],
+                      branchInfoEqual);
+}
+
+function testSingleExitFoundIfOnlyAlternateExitDefined() {
+    let foundBranchesForOnlyAlternateDefinition =
+        parseScriptForBranches("let a, b;\n" +
+                               "if (1);\n" +
+                               "else\n" +
+                               "    a++;\n");
+    assertArrayEquals(foundBranchesForOnlyAlternateDefinition,
+                      [
+                          { point: 2, exits: [4] }
+                      ],
+                      branchInfoEqual);
+}
+
+function testImplicitBranchFoundForWhileStatement() {
+    let foundBranchesForWhileStatement =
+        parseScriptForBranches("while (1) {\n" +
+                               "    let a = 1;\n" +
+                               "}\n" +
+                               "let b = 2;");
+    assertArrayEquals(foundBranchesForWhileStatement,
+                      [
+                          { point: 1, exits: [2] }
+                      ],
+                      branchInfoEqual);
+}
+
+function testImplicitBranchFoundForDoWhileStatement() {
+    let foundBranchesForDoWhileStatement =
+        parseScriptForBranches("do {\n" +
+                               "    let a = 1;\n" +
+                               "} while (1)\n" +
+                               "let b = 2;");
+    assertArrayEquals(foundBranchesForDoWhileStatement,
+                      [
+                          { point: 1, exits: [2] }
+                      ],
+                      branchInfoEqual);
+}
+
+function testImplicitBranchFoundForDoWhileStatement() {
+    let foundBranchesForDoWhileStatement =
+        parseScriptForBranches("do {\n" +
+                               "    let a = 1;\n" +
+                               "} while (1)\n" +
+                               "let b = 2;");
+    assertArrayEquals(foundBranchesForDoWhileStatement,
+                      [
+                          /* For do-while loops the branch-point is
+                           * at the 'do' condition and not the
+                           * 'while' */
+                          { point: 1, exits: [2] }
+                      ],
+                      branchInfoEqual);
+}
+
+function testAllExitsFoundForCaseStatements() {
+    let foundExitsInCaseStatement =
+        parseScriptForBranches("let a = 1;\n" +
+                               "switch (1) {\n" +
+                               "case '1':\n" +
+                               "    a++;\n" +
+                               "    break;\n" +
+                               "case '2':\n" +
+                               "    a++\n" +
+                               "    break;\n" +
+                               "default:\n" +
+                               "    a++\n" +
+                               "    break;\n" +
+                               "}\n");
+    assertArrayEquals(foundExitsInCaseStatement,
+                      [
+                          /* There are three potential exits here */
+                          { point: 2, exits: [4, 7, 10] }
+                      ],
+                      branchInfoEqual);
+}
+
+function testAllExitsFoundForFallthroughCaseStatements() {
+    let foundExitsInCaseStatement =
+        parseScriptForBranches("let a = 1;\n" +
+                               "switch (1) {\n" +
+                               "case '1':\n" +
+                               "case 'a':\n" +
+                               "case 'b':\n" +
+                               "    a++;\n" +
+                               "    break;\n" +
+                               "case '2':\n" +
+                               "    a++\n" +
+                               "    break;\n" +
+                               "default:\n" +
+                               "    a++\n" +
+                               "    break;\n" +
+                               "}\n");
+    assertArrayEquals(foundExitsInCaseStatement,
+                      [
+                          /* There are three potential exits here */
+                          { point: 2, exits: [6, 9, 12] }
+                      ],
+                      branchInfoEqual);
+}
+
+function testAllNoExitsFoundForCaseStatementsWithNoopLabels() {
+    let foundExitsInCaseStatement =
+        parseScriptForBranches("let a = 1;\n" +
+                               "switch (1) {\n" +
+                               "case '1':\n" +
+                               "case '2':\n" +
+                               "default:\n" +
+                               "}\n");
+    assertArrayEquals(foundExitsInCaseStatement,
+                      [],
+                      branchInfoEqual);
+}
+
+
+JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
diff --git a/modules/infoReflect.js b/modules/infoReflect.js
new file mode 100644
index 0000000..b1d5cab
--- /dev/null
+++ b/modules/infoReflect.js
@@ -0,0 +1,338 @@
+/*
+ * Copyright (c) 2014 Endless Mobile, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ *
+ * Authored By: Sam Spilsbury <sam endlessm com>
+ */
+
+function getSubNodesForNode(node) {
+    let subNodes = [];
+    switch (node.type) {
+    /* These statements have a single body */
+    case 'LabelledStatement':
+    case 'WithStatement':
+    case 'LetStatement':
+    case 'ForInStatement':
+    case 'ForOfStatement':
+    case 'FunctionDeclaration':
+    case 'FunctionExpression':
+    case 'ArrowExpression':
+    case 'CatchClause':
+        subNodes.push(node.body);
+        break;
+    case 'WhileStatement':
+    case 'DoWhileStatement':
+        subNodes.push(node.body);
+        subNodes.push(node.test);
+        break;
+    case 'ForStatement':
+        if (node.init !== null)
+            subNodes.push(node.init);
+        if (node.test !== null)
+            subNodes.push(node.test);
+        if (node.update !== null)
+            subNodes.push(node.update);
+
+        subNodes.push(node.body);
+        break;
+    case 'BlockStatement':
+        Array.prototype.push.apply(subNodes, node.body);
+        break;
+    case 'ThrowStatement':
+    case 'ReturnStatement':
+        if (node.argument !== null)
+            subNodes.push(node.argument);
+        break;
+    case 'ExpressionStatement':
+        subNodes.push(node.expression);
+        break;
+    case 'AssignmentExpression':
+        subNodes.push(node.left, node.right);
+        break;
+    case 'ObjectExpression':
+        node.properties.forEach(function(prop) {
+            subNodes.push(prop.value);
+        });
+        break;
+    /* It is very possible that there might be something
+     * interesting in the function arguments, so we need to
+     * walk them too */
+    case 'NewExpression':
+    case 'CallExpression':
+        Array.prototype.push.apply(subNodes, node.arguments);
+        subNodes.push(node.callee);
+        break;
+    /* These statements might have multiple different bodies
+     * depending on whether or not they were entered */
+    case 'IfStatement':
+        subNodes = [node.test, node.consequent];
+        if (node.alternate !== null)
+            subNodes.push(node.alternate);
+        break;
+    case 'TryStatement':
+        subNodes = [node.block];
+        if (node.handler !== null)
+            subNodes.push(node.handler);
+        if (node.finalizer !== null)
+            subNodes.push(node.finalizer);
+        break;
+    case 'SwitchStatement':
+        for (let caseClause of node.cases) {
+            caseClause.consequent.forEach(function(expression) {
+                subNodes.push(expression);
+            });
+        }
+
+        break;
+    /* Variable declarations might be initialized to
+     * some expression, so traverse the tree and see if
+     * we can get into the expression */
+    case 'VariableDeclaration':
+        node.declarations.forEach(function (declarator) {
+            if (declarator.init !== null)
+                subNodes.push(declarator.init);
+        });
+
+        break;
+    }
+
+    return subNodes;
+}
+
+function collectForSubNodes(subNodes, collector) {
+    let result = [];
+    if (subNodes !== undefined &&
+        subNodes.length > 0) {
+
+        subNodes.forEach(function(node) {
+            let subResult = collector(node);
+            if (subResult !== undefined)
+                Array.prototype.push.apply(result, subResult);
+
+            Array.prototype.push.apply(result,
+                                       collectForSubNodes(getSubNodesForNode(node),
+                                                          collector));
+        });
+    }
+
+    return result;
+}
+
+/* Unfortunately, the Reflect API doesn't give us enough information to
+ * uniquely identify a function. A function might be anonymous, in which
+ * case the JS engine uses some heurisitics to get a unique string identifier
+ * but that isn't available to us here.
+ *
+ * There's also the edge-case where functions with the same name might be
+ * defined within the same scope, or multiple anonymous functions might
+ * be defined on the same line. In that case, it will look like we entered
+ * the same function multiple times since we can't get column information
+ * from the engine-side.
+ *
+ * For instance:
+ *
+ * 1. function f() {
+ *       function f() {
+ *       }
+ *    }
+ *
+ * 2. let a = function() { function(a, b) {} };
+ *
+ * 3. let a = function() { function () {} }
+ *
+ * We can work-around case 1 by using the line numbers to get a unique identifier.
+ * We can work-around case 2 by using the arguments length to get a unique identifier
+ * We can't work-around case 3. The best thing we can do is warn that coverage
+ * reports might be inaccurate as a result */
+function functionsForNode(node) {
+    let functionNames = [];
+    switch (node.type) {
+    case 'FunctionDeclaration':
+    case 'FunctionExpression':
+        if (node.id !== null) {
+            functionNames.push({ name: node.id.name,
+                                 line: node.loc.start.line,
+                                 n_params: node.params.length });
+        }
+        /* If the function wasn't found, we just push a name
+         * that looks like 'function:lineno' to signify that
+         * this was an anonymous function. If the coverage tool
+         * enters a function with no name (but a line number)
+         * then it can probably use this information to
+         * figure out which function it was */
+        else {
+            functionNames.push({ name: null,
+                                 line: node.loc.start.line,
+                                 n_params: node.params.length });
+        }
+    }
+
+    return functionNames;
+}
+
+function functionsForAST(ast) {
+    return collectForSubNodes(ast.body, functionsForNode);
+}
+
+/* If a branch' consequent is a block statement, there's
+ * a chance that it could start on the same line, although
+ * that's not where execution really starts. If it is
+ * a block statement then handle the case and go
+ * to the first line where execution starts */
+function getBranchExitStartLine(branchBodyNode) {
+    switch (branchBodyNode.type) {
+    case 'BlockStatement':
+        /* Hit a block statement, but nothing inside, can never
+         * be executed, tell the upper level to move on to the next
+         * statement */
+        if (branchBodyNode.body.length === 0)
+            return -1;
+
+        /* Handle the case where we have nested block statements
+         * that never actually get to executable code by handling
+         * all statements within a block */
+        for (let statement of branchBodyNode.body) {
+            let startLine = getBranchExitStartLine(statement);
+            if (startLine !== -1)
+                return startLine;
+        }
+
+        /* Couldn't find an executable line inside this block */
+        return -1;
+
+    case 'SwitchCase':
+        /* Hit a switch, but nothing inside, can never
+         * be executed, tell the upper level to move on to the next
+         * statement */
+        if (branchBodyNode.consequent.length === 0)
+            return -1;
+
+        /* Handle the case where we have nested block statements
+         * that never actually get to executable code by handling
+         * all statements within a block */
+        for (let statement of branchBodyNode.consequent) {
+            let startLine = getBranchExitStartLine(statement);
+            if (startLine !== -1) {
+                return startLine;
+            }
+        }
+
+        /* Couldn't find an executable line inside this block */
+        return -1;
+    /* These types of statements are never executable */
+    case 'EmptyStatement':
+    case 'LabelledStatement':
+        return -1;
+    default:
+        break;
+    }
+
+    return branchBodyNode.loc.start.line;
+}
+
+function branchesForNode(node) {
+    let branches = [];
+
+    let branchExitNodes = [];
+    switch (node.type) {
+    case 'IfStatement':
+        branchExitNodes.push(node.consequent);
+        if (node.alternate !== null)
+            branchExitNodes.push(node.alternate);
+        break;
+    case 'WhileStatement':
+    case 'DoWhileStatement':
+        branchExitNodes.push(node.body);
+        break;
+    case 'SwitchStatement':
+
+        /* The case clauses by themselves are never executable
+         * so find the actual exits */
+        Array.prototype.push.apply(branchExitNodes, node.cases);
+        break;
+    default:
+        break;
+    }
+
+    let branchExitStartLines = branchExitNodes.map(getBranchExitStartLine);
+    branchExitStartLines = branchExitStartLines.filter(function(line) {
+        return line !== -1;
+    });
+
+    /* Branch must have at least one exit */
+    if (branchExitStartLines.length) {
+        branches.push({ point: node.loc.start.line,
+                        exits: branchExitStartLines });
+    }
+
+    return branches;
+}
+
+function branchesForAST(ast) {
+    return collectForSubNodes(ast.body, branchesForNode);
+}
+
+function expressionLinesForNode(statement) {
+    let expressionLines = [];
+
+    let expressionNodeTypes = ['Expression',
+                               'Declaration',
+                               'Statement',
+                               'Clause',
+                               'Literal',
+                               'Identifier'];
+
+    if (expressionNodeTypes.some(function(type) {
+            return statement.type.indexOf(type) !== -1;
+        })) {
+
+        /* These expressions aren't executable on their own */
+        switch (statement.type) {
+        case 'FunctionDeclaration':
+        case 'LiteralExpression':
+            break;
+        /* Perplexingly, an empty block statement is actually executable,
+         * push it if it is */
+        case 'BlockStatement':
+            if (statement.body.length !== 0)
+                break;
+            expressionLines.push(statement.loc.start.line);
+            break;
+        default:
+            expressionLines.push(statement.loc.start.line);
+            break;
+        }
+    }
+
+    return expressionLines;
+}
+
+function deduplicate(list) {
+    return list.filter(function(elem, pos, self) {
+        return self.indexOf(elem) === pos;
+    });
+}
+
+function expressionLinesForAST(ast) {
+    let allExpressions = collectForSubNodes(ast.body, expressionLinesForNode);
+    allExpressions = deduplicate(allExpressions);
+
+    return allExpressions;
+}
diff --git a/modules/modules.gresource.xml.in b/modules/modules.gresource.xml.in
index 8fccdfd..01436bd 100644
--- a/modules/modules.gresource.xml.in
+++ b/modules/modules.gresource.xml.in
@@ -14,6 +14,7 @@
     <file>modules/gettext.js</file>
     <file>modules/lang.js</file>
     <file>modules/mainloop.js</file>
+    <file>modules/infoReflect.js</file>
     <file>modules/jsUnit.js</file>
     <file>modules/signals.js</file>
     <file>modules/format.js</file>
diff --git a/test/gjs-test-reflected-script.cpp b/test/gjs-test-reflected-script.cpp
new file mode 100644
index 0000000..de3ec1e
--- /dev/null
+++ b/test/gjs-test-reflected-script.cpp
@@ -0,0 +1,419 @@
+/*
+ * Copyright © 2014 Endless Mobile, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Authored By: Sam Spilsbury <sam endlessm com>
+ */
+
+#include <errno.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <glib.h>
+#include <gio/gio.h>
+#include <gjs/gjs.h>
+#include <gjs/reflected-script.h>
+
+typedef struct _GjsReflectedScriptTestFixture {
+    char       *temporary_js_script_filename;
+    int        temporary_js_script_open_handle;
+    GjsContext *reflection_context;
+} GjsReflectedScriptTestFixture;
+
+static void
+gjs_reflected_test_fixture_set_up(gpointer      fixture_data,
+                                  gconstpointer test_data)
+{
+    GjsReflectedScriptTestFixture *fixture = (GjsReflectedScriptTestFixture *) fixture_data;
+    char                          *current_dir = g_get_current_dir();
+    fixture->temporary_js_script_open_handle = g_file_open_tmp("mock-js-XXXXXXX.js",
+                                                               &fixture->temporary_js_script_filename,
+                                                               NULL);
+    fixture->reflection_context = gjs_reflected_script_create_reflection_context();
+
+    g_free(current_dir);
+}
+
+static void
+gjs_reflected_test_fixture_tear_down(gpointer      fixture_data,
+                                     gconstpointer test_data)
+{
+    GjsReflectedScriptTestFixture *fixture = (GjsReflectedScriptTestFixture *) fixture_data;
+    unlink(fixture->temporary_js_script_filename);
+    g_free(fixture->temporary_js_script_filename);
+    close(fixture->temporary_js_script_open_handle);
+
+    g_object_unref(fixture->reflection_context);
+}
+
+static void
+test_reflect_creation_and_destruction(gpointer      fixture_data,
+                                      gconstpointer user_data)
+{
+    GjsReflectedScriptTestFixture *fixture = (GjsReflectedScriptTestFixture *) fixture_data;
+
+    const char mock_script[] = "var a = 1;\n";
+
+    if (write(fixture->temporary_js_script_open_handle, mock_script, strlen(mock_script) * sizeof(char)) == 
-1)
+        g_error("Failed to write to test script");
+
+    GjsReflectedScript *script = gjs_reflected_script_new(fixture->temporary_js_script_filename,
+                                                          fixture->reflection_context);
+    g_object_unref(script);
+}
+
+static gboolean
+integer_arrays_equal(const unsigned int *actual,
+                     unsigned int        actual_n,
+                     const unsigned int *expected,
+                     unsigned int        expected_n)
+{
+    if (actual_n != expected_n)
+        return FALSE;
+
+    unsigned int i;
+
+    for (i = 0; i < actual_n; ++i)
+        if (actual[i] != expected[i])
+            return FALSE;
+
+    return TRUE;
+}
+
+static GjsReflectedScript *
+get_reflected_script_for(const char *script,
+                         int         file,
+                         const char *filename,
+                         GjsContext *reflection_context)
+{
+    if (write(file, script, strlen(script) * sizeof(char)) == -1)
+        g_error("Failed to write to test script");
+
+    GjsReflectedScript *reflected_script =
+        gjs_reflected_script_new(filename, reflection_context);
+    return reflected_script;
+}
+
+static void
+test_reflect_get_all_executable_expression_lines(gpointer      fixture_data,
+                                                 gconstpointer user_data)
+{
+    GjsReflectedScriptTestFixture *fixture = (GjsReflectedScriptTestFixture *) fixture_data;
+
+    const char mock_script[] =
+            "var a = 1.0;\n"
+            "var b = 2.0;\n"
+            "var c = 3.0;\n";
+
+    GjsReflectedScript *script = get_reflected_script_for(mock_script,
+                                                          fixture->temporary_js_script_open_handle,
+                                                          fixture->temporary_js_script_filename,
+                                                          fixture->reflection_context);
+    unsigned int       n_executable_lines = 0;
+    const unsigned int *executable_lines = gjs_reflected_script_get_expression_lines(script,
+                                                                                     &n_executable_lines);
+
+    const unsigned int expected_executable_lines[] = {
+        1, 2, 3
+    };
+    const unsigned int n_expected_executable_lines = G_N_ELEMENTS(expected_executable_lines);
+
+    g_assert(integer_arrays_equal(executable_lines,
+                                  n_executable_lines,
+                                  expected_executable_lines,
+                                  n_expected_executable_lines));
+
+    g_object_unref(script);
+}
+
+typedef struct _GjsReflectedScriptExpectedBranch {
+    unsigned int point;
+    unsigned int alternatives[256];
+    unsigned int n_alternatives;
+} ExpectedBranch;
+
+static gboolean
+branch_info_equal(ExpectedBranch                     *expected,
+                  const GjsReflectedScriptBranchInfo *branch)
+{
+    if (gjs_reflected_script_branch_info_get_branch_point(branch) != expected->point)
+        return FALSE;
+
+    unsigned int       n_actual_alternatives = 0;
+    const unsigned int *actual_alternatives = 
gjs_reflected_script_branch_info_get_branch_alternatives(branch,
+                                                                                                       
&n_actual_alternatives);
+    unsigned int       n_expected_alternatives = expected->n_alternatives;
+    const unsigned int *expected_alternatives = expected->alternatives;
+
+    return integer_arrays_equal(actual_alternatives,
+                                n_actual_alternatives,
+                                expected_alternatives,
+                                n_expected_alternatives);
+}
+
+static gboolean
+has_elements_in_branch_array_in_order (ExpectedBranch                     *expected,
+                                       const GjsReflectedScriptBranchInfo **branches,
+                                       unsigned int                        n_expected)
+{
+    const GjsReflectedScriptBranchInfo **branch_iterator = branches;
+    unsigned int i;
+
+    for (i = 0; i < n_expected; ++i, ++branch_iterator) {
+        /* Size mismatch */
+        if (!(*branch_iterator))
+            return FALSE;
+
+        if (!branch_info_equal(&expected[i],
+                               *branch_iterator))
+            return FALSE;
+    }
+
+    /* Size mismatch - there are still remaining branches */
+    if (*branch_iterator)
+        return FALSE;
+
+    return TRUE;
+}
+
+static void
+test_reflect_finds_branches(gpointer      fixture_data,
+                            gconstpointer user_data)
+{
+    GjsReflectedScriptTestFixture *fixture = (GjsReflectedScriptTestFixture *) fixture_data;
+
+    const char mock_script[] =
+        "let a, b;\n"
+        "if (1)\n"
+        "    a = 1.0\n"
+        "else\n"
+        "    b = 2.0\n"
+        "\n";
+
+    GjsReflectedScript *script = get_reflected_script_for(mock_script,
+                                                          fixture->temporary_js_script_open_handle,
+                                                          fixture->temporary_js_script_filename,
+                                                          fixture->reflection_context);
+    const GjsReflectedScriptBranchInfo **branches =
+        gjs_reflected_script_get_branches(script);
+
+    ExpectedBranch expected[] = {
+        {
+            2,
+            { 3, 5 },
+            2
+        }
+    };
+
+    g_assert(has_elements_in_branch_array_in_order(expected,
+                                                   branches,
+                                                   G_N_ELEMENTS(expected)));
+
+    g_object_unref(script);
+}
+
+typedef struct _ExpectedReflectedFunction {
+    unsigned int line;
+    unsigned int n_params;
+    const char   *name;
+} ExpectedReflectedFunction;
+
+static gboolean
+function_info_equal(const ExpectedReflectedFunction      *expected,
+                    const GjsReflectedScriptFunctionInfo *actual)
+{
+    const gchar  *actual_name = gjs_reflected_script_function_info_get_name(actual);
+    unsigned int actual_line = gjs_reflected_script_function_info_get_line_number(actual);
+    unsigned int actual_n_params = gjs_reflected_script_function_info_get_n_params(actual);
+
+    if ((actual_name && !expected->name) ||
+        (!actual_name && expected->name))
+        return FALSE;
+
+    if (g_strcmp0(actual_name, expected->name) != 0)
+        return FALSE;
+
+    return actual_line == expected->line &&
+           actual_n_params == expected->n_params;
+}
+
+static gboolean
+has_elements_in_function_array_in_order(const GjsReflectedScriptFunctionInfo **functions,
+                                        const ExpectedReflectedFunction      *expected,
+                                        unsigned int                          n_expected)
+{
+    const GjsReflectedScriptFunctionInfo **function_iterator = functions;
+    unsigned int i;
+
+    for (i = 0; i < n_expected; ++i, ++function_iterator) {
+        /* Size mismatch */
+        if (!(*function_iterator))
+            return FALSE;
+
+        if (!function_info_equal(&expected[i],
+                                 *function_iterator))
+            return FALSE;
+    }
+
+    /* Size mismatch - there are still remaining functions */
+    if (*function_iterator)
+        return FALSE;
+
+    return TRUE;
+}
+
+static void
+test_reflect_finds_functions(gpointer      fixture_data,
+                             gconstpointer user_data)
+{
+    GjsReflectedScriptTestFixture *fixture = (GjsReflectedScriptTestFixture *) fixture_data;
+
+    const char mock_script[] =
+        "function f1() {}\n"
+        "function f2() {}\n"
+        "function f3() {}\n";
+
+    GjsReflectedScript *script = get_reflected_script_for(mock_script,
+                                                          fixture->temporary_js_script_open_handle,
+                                                          fixture->temporary_js_script_filename,
+                                                          fixture->reflection_context);
+    const GjsReflectedScriptFunctionInfo **functions =
+        gjs_reflected_script_get_functions(script);
+
+    ExpectedReflectedFunction expected[] = {
+        { 1, 0, "f1" },
+        { 2, 0, "f2" },
+        { 3, 0, "f3" }
+    };
+
+    g_assert(has_elements_in_function_array_in_order(functions,
+                                                     expected,
+                                                     G_N_ELEMENTS(expected)));
+
+    g_object_unref(script);
+}
+
+static void
+test_reflect_get_n_lines(gpointer      fixture_data,
+                         gconstpointer user_data)
+{
+    GjsReflectedScriptTestFixture *fixture = (GjsReflectedScriptTestFixture *) fixture_data;
+
+    const char mock_script[] =
+        "function f1() {}\n"
+        "function f2() {}\n"
+        "function f3() {}\n";
+
+    GjsReflectedScript *script = get_reflected_script_for(mock_script,
+                                                          fixture->temporary_js_script_open_handle,
+                                                          fixture->temporary_js_script_filename,
+                                                          fixture->reflection_context);
+    unsigned int n_lines = gjs_reflected_script_get_n_lines(script);
+
+    g_assert(n_lines == 4);
+
+    g_object_unref(script);
+}
+
+static void
+silence_log_handler(const char     *log_domain,
+                    GLogLevelFlags  flags,
+                    const char     *message,
+                    gpointer        user_data)
+{
+}
+
+static void
+test_reflect_on_nonexistent_script_returns_empty(gpointer      fixture_data,
+                                                 gconstpointer user_data)
+{
+    GjsReflectedScriptTestFixture *fixture =
+        (GjsReflectedScriptTestFixture *) fixture_data;
+    GjsReflectedScript *script = gjs_reflected_script_new("doesnotexist://does_not_exist",
+                                                          fixture->reflection_context);
+
+    /* Make the handler shut up so that we don't get an assertion on
+     * raised warnings from bad scripts */
+    GLogLevelFlags old_flags = g_log_set_always_fatal((GLogLevelFlags) G_LOG_LEVEL_ERROR);
+    GLogFunc old_handler = g_log_set_default_handler(silence_log_handler, NULL);
+
+    const GjsReflectedScriptFunctionInfo **functions =
+        gjs_reflected_script_get_functions(script);
+    const GjsReflectedScriptBranchInfo **branches =
+        gjs_reflected_script_get_branches(script);
+    unsigned int n_expression_lines;
+    const unsigned int *lines =
+        gjs_reflected_script_get_expression_lines(script, &n_expression_lines);
+    unsigned int n_lines = gjs_reflected_script_get_n_lines(script);
+
+    g_assert(*functions == NULL);
+    g_assert(*branches == NULL);
+    g_assert(*lines == 0);
+    g_assert(n_lines == 0);
+    g_assert(n_expression_lines == 0);
+
+    g_log_set_default_handler(old_handler, NULL);
+    g_log_set_always_fatal(old_flags);
+}
+
+typedef struct _TestFixture
+{
+    GTestFixtureFunc set_up;
+    GTestFixtureFunc tear_down;
+    size_t           test_fixture_size;
+} TestFixture;
+
+static void
+add_test_for_fixture(const char       *name,
+                     TestFixture      *fixture,
+                     GTestFixtureFunc  test_func)
+{
+    g_test_add_vtable(name,
+                      fixture->test_fixture_size,
+                      NULL,
+                      fixture->set_up,
+                      test_func,
+                      fixture->tear_down);
+}
+
+void
+gjs_test_add_tests_for_reflected_script(void)
+{
+    TestFixture reflected_script_default_fixture = {
+        gjs_reflected_test_fixture_set_up,
+        gjs_reflected_test_fixture_tear_down,
+        sizeof(GjsReflectedScriptTestFixture)
+    };
+
+    add_test_for_fixture("/gjs/reflected_script/construction",
+                         &reflected_script_default_fixture,
+                         test_reflect_creation_and_destruction);
+    add_test_for_fixture("/gjs/reflected_script/all_are_expression_lines",
+                         &reflected_script_default_fixture,
+                         test_reflect_get_all_executable_expression_lines);
+    add_test_for_fixture("/gjs/reflected_script/finds_branches",
+                         &reflected_script_default_fixture,
+                         test_reflect_finds_branches);
+    add_test_for_fixture("/gjs/reflected_script/finds_functions",
+                         &reflected_script_default_fixture,
+                         test_reflect_finds_functions);
+    add_test_for_fixture("/gjs/reflected_script/n_lines",
+                         &reflected_script_default_fixture,
+                         test_reflect_get_n_lines);
+    add_test_for_fixture("/gjs/reflected/script/nonexistent",
+                         &reflected_script_default_fixture,
+                         test_reflect_on_nonexistent_script_returns_empty);
+}
diff --git a/test/gjs-tests-add-funcs.h b/test/gjs-tests-add-funcs.h
new file mode 100644
index 0000000..e872f1c
--- /dev/null
+++ b/test/gjs-tests-add-funcs.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright © 2013 Endless Mobile, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Authored By: Sam Spilsbury <sam endlessm com>
+ */
+#ifndef GJS_TESTS_ADD_FUNCS_H
+#define GJS_TESTS_ADD_FUNCS_H
+
+void gjs_test_add_tests_for_reflected_script ();
+
+#endif
diff --git a/test/gjs-tests.cpp b/test/gjs-tests.cpp
index b3a02fe..faf75f7 100644
--- a/test/gjs-tests.cpp
+++ b/test/gjs-tests.cpp
@@ -28,6 +28,8 @@
 #include <util/glib.h>
 #include <util/crash.h>
 
+#include "gjs-tests-add-funcs.h"
+
 typedef struct _GjsUnitTestFixture GjsUnitTestFixture;
 
 struct _GjsUnitTestFixture {
@@ -396,6 +398,8 @@ main(int    argc,
     g_test_add_func("/util/glib/strv/concat/null", gjstest_test_func_util_glib_strv_concat_null);
     g_test_add_func("/util/glib/strv/concat/pointers", gjstest_test_func_util_glib_strv_concat_pointers);
 
+    gjs_test_add_tests_for_reflected_script();
+
     g_test_run();
 
     return 0;



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]