[gnome-shell] lookingGlass: Add tab-completion



commit 3941961f8b10d565c527365f7fbc1971df50fd95
Author: Jason Siefken <siefkenj gmail com>
Date:   Sat Oct 29 01:20:49 2011 -0700

    lookingGlass: Add tab-completion
    
    https://bugzilla.gnome.org/show_bug.cgi?id=661054

 data/theme/gnome-shell.css |    6 +
 js/Makefile.am             |    1 +
 js/misc/jsParse.js         |  246 ++++++++++++++++++++++++++++++++++++++++++++
 js/ui/lookingGlass.js      |  156 +++++++++++++++++++++++++++-
 tests/unit/jsParse.js      |  194 ++++++++++++++++++++++++++++++++++
 5 files changed, 599 insertions(+), 4 deletions(-)
---
diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index ea81870..1e876f6 100644
--- a/data/theme/gnome-shell.css
+++ b/data/theme/gnome-shell.css
@@ -858,6 +858,12 @@ StTooltip StLabel {
     selected-color: #333333;
 }
 
+.lg-completions-text
+{
+    font-size: .9em;
+    font-style: italic;
+}
+
 .lg-obj-inspector-title
 {
     spacing: 4px;
diff --git a/js/Makefile.am b/js/Makefile.am
index 58e0489..ca7756e 100644
--- a/js/Makefile.am
+++ b/js/Makefile.am
@@ -13,6 +13,7 @@ nobase_dist_js_DATA = 	\
 	misc/format.js		\
 	misc/gnomeSession.js	\
 	misc/history.js		\
+	misc/jsParse.js		\
 	misc/modemManager.js	\
 	misc/params.js		\
 	misc/screenSaver.js     \
diff --git a/js/misc/jsParse.js b/js/misc/jsParse.js
new file mode 100644
index 0000000..7f0c707
--- /dev/null
+++ b/js/misc/jsParse.js
@@ -0,0 +1,246 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+// Returns a list of potential completions for text. Completions either
+// follow a dot (e.g. foo.ba -> bar) or they are picked from globalCompletionList (e.g. fo -> foo)
+// commandHeader is prefixed on any expression before it is eval'ed.  It will most likely
+// consist of global constants that might not carry over from the calling environment.
+//
+// This function is likely the one you want to call from external modules
+function getCompletions(text, commandHeader, globalCompletionList) {
+    let methods = [];
+    let expr, base;
+    let attrHead = '';
+    if (globalCompletionList == null) {
+        globalCompletionList = [];
+    }
+
+    let offset = getExpressionOffset(text, text.length - 1);
+    if (offset >= 0) {
+        text = text.slice(offset);
+
+        // Look for expressions like "Main.panel.foo" and match Main.panel and foo
+        let matches = text.match(/(.*)\.(.*)/);
+        if (matches) {
+            [expr, base, attrHead] = matches;
+
+            methods = getPropertyNamesFromExpression(base, commandHeader).filter(function(attr) {
+                return attr.slice(0, attrHead.length) == attrHead;
+            });
+        }
+
+        // Look for the empty expression or partially entered words
+        // not proceeded by a dot and match them against global constants
+        matches = text.match(/^(\w*)$/);
+        if (text == '' || matches) {
+            [expr, attrHead] = matches;
+            methods = globalCompletionList.filter(function(attr) {
+                return attr.slice(0, attrHead.length) == attrHead;
+            });
+        }
+    }
+
+    return [methods, attrHead];
+}
+
+
+//
+// A few functions for parsing strings of javascript code.
+//
+
+// Identify characters that delimit an expression.  That is,
+// if we encounter anything that isn't a letter, '.', ')', or ']',
+// we should stop parsing.
+function isStopChar(c) {
+    return !c.match(/[\w\.\)\]]/);
+}
+
+// Given the ending position of a quoted string, find where it starts
+function findMatchingQuote(expr, offset) {
+    let quoteChar = expr.charAt(offset);
+    for (let i = offset - 1; i >= 0; --i) {
+        if (expr.charAt(i) == quoteChar && expr.charAt(i-1) != '\\'){
+            return i;
+        }
+    }
+    return -1;
+}
+
+// Given the ending position of a regex, find where it starts
+function findMatchingSlash(expr, offset) {
+    for (let i = offset - 1; i >= 0; --i) {
+        if (expr.charAt(i) == '/' && expr.charAt(i-1) != '\\'){
+            return i;
+        }
+    }
+    return -1;
+}
+
+// If expr.charAt(offset) is ')' or ']',
+// return the position of the corresponding '(' or '[' bracket.
+// This function does not check for syntactic correctness.  e.g.,
+// findMatchingBrace("[(])", 3) returns 1.
+function findMatchingBrace(expr, offset) {
+    let closeBrace = expr.charAt(offset);
+    let openBrace = ({')': '(', ']': '['})[closeBrace];
+
+    function findTheBrace(expr, offset) {
+        if (offset < 0) {
+            return -1;
+        }
+
+        if (expr.charAt(offset) == openBrace) {
+            return offset;
+        }
+        if (expr.charAt(offset).match(/['"]/)) {
+            return findTheBrace(expr, findMatchingQuote(expr, offset) - 1);
+        }
+        if (expr.charAt(offset) == '/') {
+            return findTheBrace(expr, findMatchingSlash(expr, offset) - 1);
+        }
+        if (expr.charAt(offset) == closeBrace) {
+            return findTheBrace(expr, findTheBrace(expr, offset - 1) - 1);
+        }
+
+        return findTheBrace(expr, offset - 1);
+
+    }
+
+    return findTheBrace(expr, offset - 1);
+}
+
+// Walk expr backwards from offset looking for the beginning of an
+// expression suitable for passing to eval.
+// There is no guarantee of correct javascript syntax between the return
+// value and offset.  This function is meant to take a string like
+// "foo(Obj.We.Are.Completing" and allow you to extract "Obj.We.Are.Completing"
+function getExpressionOffset(expr, offset) {
+    while (offset >= 0) {
+        let currChar = expr.charAt(offset);
+
+        if (isStopChar(currChar)){
+            return offset + 1;
+        }
+
+        if (currChar.match(/[\)\]]/)) {
+            offset = findMatchingBrace(expr, offset);
+        }
+
+        --offset;
+    }
+
+    return offset + 1;
+}
+
+// Things with non-word characters or that start with a number
+// are not accessible via .foo notation and so aren't returned
+function isValidPropertyName(w) {
+    return !(w.match(/\W/) || w.match(/^\d/));
+}
+
+// To get all properties (enumerable and not), we need to walk
+// the prototype chain ourselves
+function getAllProps(obj) {
+    if (obj === null || obj === undefined) {
+        return [];
+    }
+    return Object.getOwnPropertyNames(obj).concat( getAllProps(Object.getPrototypeOf(obj)) );
+}
+
+// Given a string _expr_, returns all methods
+// that can be accessed via '.' notation.
+// e.g., expr="({ foo: null, bar: null, 4: null })" will
+// return ["foo", "bar", ...] but the list will not include "4",
+// since methods accessed with '.' notation must star with a letter or _.
+function getPropertyNamesFromExpression(expr, commandHeader) {
+    if (commandHeader == null) {
+        commandHeader = '';
+    }
+
+    let obj = {};
+    if (!isUnsafeExpression(expr)) {
+        try {
+                obj = eval(commandHeader + expr);
+        } catch (e) {
+            return [];
+        }
+    } else {
+        return [];
+    }
+
+    let propsUnique = {};
+    if (typeof obj === 'object'){
+        let allProps = getAllProps(obj);
+        // Get only things we are allowed to complete following a '.'
+        allProps = allProps.filter( isValidPropertyName );
+
+        // Make sure propsUnique contains one key for every
+        // property so we end up with a unique list of properties
+        allProps.map(function(p){ propsUnique[p] = null; });
+    }
+    return Object.keys(propsUnique).sort();
+}
+
+// Given a list of words, returns the longest prefix they all have in common
+function getCommonPrefix(words) {
+    let word = words[0];
+    for (let i = 0; i < word.length; i++) {
+        for (let w = 1; w < words.length; w++) {
+            if (words[w].charAt(i) != word.charAt(i))
+                return word.slice(0, i);
+        }
+    }
+    return word;
+}
+
+// Returns true if there is reason to think that eval(str)
+// will modify the global scope
+function isUnsafeExpression(str) {
+    // Remove any blocks that are quoted or are in a regex
+    function removeLiterals(str) {
+        if (str.length == 0) {
+            return '';
+        }
+
+        let currChar = str.charAt(str.length - 1);
+        if (currChar == '"' || currChar == '\'') {
+            return removeLiterals(str.slice(0, findMatchingQuote(str, str.length - 1)));
+        } else if (currChar == '/') {
+            return removeLiterals(str.slice(0, findMatchingSlash(str, str.length - 1)));
+        }
+
+        return removeLiterals(str.slice(0, str.length - 1)) + currChar;
+    }
+
+    // Check for any sort of assignment
+    // The strategy used is dumb: remove any quotes
+    // or regexs and comparison operators and see if there is an '=' character.
+    // If there is, it might be an unsafe assignment.
+
+    let prunedStr = removeLiterals(str);
+    prunedStr = prunedStr.replace(/[=!]==/g, '');    //replace === and !== with nothing
+    prunedStr = prunedStr.replace(/[=<>!]=/g, '');    //replace ==, <=, >=, != with nothing
+
+    if (prunedStr.match(/=/)) {
+        return true;
+    } else if (prunedStr.match(/;/)) {
+        // If we contain a semicolon not inside of a quote/regex, assume we're unsafe as well
+        return true;
+    }
+
+    return false;
+}
+
+// Returns a list of global keywords derived from str
+function getDeclaredConstants(str) {
+    let ret = [];
+    str.split(';').forEach(function(s) {
+        let base, keyword;
+        let match = s.match(/const\s+(\w+)\s*=/);
+        if (match) {
+            [base, keyword] = match;
+            ret.push(keyword);
+        }
+    });
+
+    return ret;
+}
diff --git a/js/ui/lookingGlass.js b/js/ui/lookingGlass.js
index 4e383b3..aa73220 100644
--- a/js/ui/lookingGlass.js
+++ b/js/ui/lookingGlass.js
@@ -20,6 +20,7 @@ const Link = imports.ui.link;
 const ShellEntry = imports.ui.shellEntry;
 const Tweener = imports.ui.tweener;
 const Main = imports.ui.main;
+const JsParse = imports.misc.jsParse;
 
 /* Imports...feel free to add here as needed */
 var commandHeader = 'const Clutter = imports.gi.Clutter; ' +
@@ -41,6 +42,86 @@ var commandHeader = 'const Clutter = imports.gi.Clutter; ' +
                     'const r = Lang.bind(Main.lookingGlass, Main.lookingGlass.getResult); ';
 
 const HISTORY_KEY = 'looking-glass-history';
+// Time between tabs for them to count as a double-tab event
+const AUTO_COMPLETE_DOUBLE_TAB_DELAY = 500;
+const AUTO_COMPLETE_SHOW_COMPLETION_ANIMATION_DURATION = 0.2;
+const AUTO_COMPLETE_GLOBAL_KEYWORDS = _getAutoCompleteGlobalKeywords();
+
+function _getAutoCompleteGlobalKeywords() {
+    const keywords = ['true', 'false', 'null', 'new'];
+    // Don't add the private properties of window (i.e., ones starting with '_')
+    const windowProperties = Object.getOwnPropertyNames(window).filter(function(a){ return a.charAt(0) != '_' });
+    const headerProperties = JsParse.getDeclaredConstants(commandHeader);
+
+    return keywords.concat(windowProperties).concat(headerProperties);
+}
+
+function AutoComplete(entry) {
+    this._init(entry);
+}
+
+AutoComplete.prototype = {
+    _init: function(entry) {
+        this._entry = entry;
+        this._entry.connect('key-press-event', Lang.bind(this, this._entryKeyPressEvent));
+        this._lastTabTime = global.get_current_time();
+    },
+
+    _processCompletionRequest: function(event) {
+        if (event.completions.length == 0) {
+            return;
+        }
+        // Unique match = go ahead and complete; multiple matches + single tab = complete the common starting string;
+        // multiple matches + double tab = emit a suggest event with all possible options
+        if (event.completions.length == 1) {
+            this.additionalCompletionText(event.completions[0], event.attrHead);
+            this.emit('completion', { completion: event.completions[0], type: 'whole-word' });
+        } else if (event.completions.length > 1 && event.tabType === 'single') {
+            let commonPrefix = JsParse.getCommonPrefix(event.completions);
+
+            if (commonPrefix.length > 0) {
+                this.additionalCompletionText(commonPrefix, event.attrHead);
+                this.emit('completion', { completion: commonPrefix, type: 'prefix' });
+                this.emit('suggest', { completions: event.completions});
+            }
+        } else if (event.completions.length > 1 && event.tabType === 'double') {
+            this.emit('suggest', { completions: event.completions});
+        }
+    },
+
+    _entryKeyPressEvent: function(actor, event) {
+        let cursorPos = this._entry.clutter_text.get_cursor_position();
+        let text = this._entry.get_text();
+        if (cursorPos != -1) {
+            text = text.slice(0, cursorPos);
+        }
+        if (event.get_key_symbol() == Clutter.Tab) {
+            let [completions, attrHead] = JsParse.getCompletions(text, commandHeader, AUTO_COMPLETE_GLOBAL_KEYWORDS);
+            let currTime = global.get_current_time();
+            if ((currTime - this._lastTabTime) < AUTO_COMPLETE_DOUBLE_TAB_DELAY) {
+                this._processCompletionRequest({ tabType: 'double',
+                                                 completions: completions,
+                                                 attrHead: attrHead });
+            } else {
+                this._processCompletionRequest({ tabType: 'single',
+                                                 completions: completions,
+                                                 attrHead: attrHead });
+            }
+            this._lastTabTime = currTime;
+        }
+    },
+
+    // Insert characters of text not already included in head at cursor position.  i.e., if text="abc" and head="a",
+    // the string "bc" will be appended to this._entry
+    additionalCompletionText: function(text, head) {
+        let additionalCompletionText = text.slice(head.length);
+        let cursorPos = this._entry.clutter_text.get_cursor_position();
+
+        this._entry.clutter_text.insert_text(additionalCompletionText, cursorPos);
+    }
+};
+Signals.addSignalMethods(AutoComplete.prototype);
+
 
 function Notebook() {
     this._init();
@@ -864,15 +945,15 @@ LookingGlass.prototype = {
         this._resultsArea = new St.BoxLayout({ name: 'ResultsArea', vertical: true });
         this._evalBox.add(this._resultsArea, { expand: true });
 
-        let entryArea = new St.BoxLayout({ name: 'EntryArea' });
-        this._evalBox.add_actor(entryArea);
+        this._entryArea = new St.BoxLayout({ name: 'EntryArea' });
+        this._evalBox.add_actor(this._entryArea);
 
         let label = new St.Label({ text: 'js>>> ' });
-        entryArea.add(label);
+        this._entryArea.add(label);
 
         this._entry = new St.Entry({ can_focus: true });
         ShellEntry.addContextMenu(this._entry);
-        entryArea.add(this._entry, { expand: true });
+        this._entryArea.add(this._entry, { expand: true });
 
         this._windowList = new WindowList();
         this._windowList.connect('selected', Lang.bind(this, function(list, window) {
@@ -891,6 +972,9 @@ LookingGlass.prototype = {
         notebook.appendPage('Extensions', this._extensions.actor);
 
         this._entry.clutter_text.connect('activate', Lang.bind(this, function (o, e) {
+            // Hide any completions we are currently showing
+            this._hideCompletions();
+
             let text = o.get_text();
             // Ensure we don't get newlines in the command; the history file is
             // newline-separated.
@@ -906,6 +990,17 @@ LookingGlass.prototype = {
         this._history = new History.HistoryManager({ gsettingsKey: HISTORY_KEY, 
                                                      entry: this._entry.clutter_text });
 
+        this._autoComplete = new AutoComplete(this._entry);
+        this._autoComplete.connect('suggest', Lang.bind(this, function(a,e) {
+            this._showCompletions(e.completions);
+        }));
+        // If a completion is completed unambiguously, the currently-displayed completion
+        // suggestions become irrelevant.
+        this._autoComplete.connect('completion', Lang.bind(this, function(a,e) {
+            if (e.type == 'whole-word')
+                this._hideCompletions();
+        }));
+
         this._resize();
     },
 
@@ -950,6 +1045,59 @@ LookingGlass.prototype = {
         this._notebook.scrollToBottom(0);
     },
 
+    _showCompletions: function(completions) {
+        if (!this._completionActor) {
+            let actor = new St.BoxLayout({ vertical: true });
+
+            this._completionText = new St.Label({ name: 'LookingGlassAutoCompletionText', style_class: 'lg-completions-text' });
+            this._completionText.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+            this._completionText.clutter_text.line_wrap = true;
+            actor.add(this._completionText);
+
+            let line = new Clutter.Rectangle();
+            let padBin = new St.Bin({ x_fill: true, y_fill: true });
+            padBin.add_actor(line);
+            actor.add(padBin);
+
+            this._completionActor = actor;
+            this._evalBox.insert_before(this._completionActor, this._entryArea);
+        }
+
+        this._completionText.set_text(completions.join(', '));
+
+        // Setting the height to -1 allows us to get its actual preferred height rather than
+        // whatever was last given in set_height by Tweener.
+        this._completionActor.set_height(-1);
+        let [minHeight, naturalHeight] = this._completionText.get_preferred_height(this._resultsArea.get_width());
+
+        // Don't reanimate if we are already visible
+        if (this._completionActor.visible) {
+            this._completionActor.height = naturalHeight;
+        } else {
+            this._completionActor.show();
+            Tweener.removeTweens(this._completionActor);
+            Tweener.addTween(this._completionActor, { time: AUTO_COMPLETE_SHOW_COMPLETION_ANIMATION_DURATION / St.get_slow_down_factor(),
+                                                      transition: 'easeOutQuad',
+                                                      height: naturalHeight,
+                                                      opacity: 255
+                                                    });
+        }
+    },
+
+    _hideCompletions: function() {
+        if (this._completionActor) {
+            Tweener.removeTweens(this._completionActor);
+            Tweener.addTween(this._completionActor, { time: AUTO_COMPLETE_SHOW_COMPLETION_ANIMATION_DURATION / St.get_slow_down_factor(),
+                                                      transition: 'easeOutQuad',
+                                                      height: 0,
+                                                      opacity: 0,
+                                                      onComplete: Lang.bind(this, function () {
+                                                          this._completionActor.hide();
+                                                      })
+                                                    });
+        }
+    },
+
     _evaluate : function(command) {
         this._history.addItem(command);
 
diff --git a/tests/unit/jsParse.js b/tests/unit/jsParse.js
new file mode 100644
index 0000000..ed550c9
--- /dev/null
+++ b/tests/unit/jsParse.js
@@ -0,0 +1,194 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+// Test cases for MessageTray URLification
+
+const JsUnit = imports.jsUnit;
+
+const Environment = imports.ui.environment;
+Environment.init();
+
+const JsParse = imports.misc.jsParse;
+
+const HARNESS_COMMAND_HEADER = "let imports = obj;" +
+                               "let global = obj;" +
+                               "let Main = obj;" +
+                               "let foo = obj;" +
+                               "let r = obj;";
+
+const testsFindMatchingQuote = [
+    { input: '"double quotes"',
+      output: 0 },
+    { input: '\'single quotes\'',
+      output: 0 },
+    { input: 'some unquoted "some quoted"',
+      output: 14 },
+    { input: '"mixed \' quotes\'"',
+      output: 0 },
+    { input: '"escaped \\" quote"',
+      output: 0 }
+];
+const testsFindMatchingSlash = [
+    { input: '/slash/',
+      output: 0 },
+    { input: '/slash " with $ funny ^\' stuff/',
+      output: 0  },
+    { input: 'some unslashed /some slashed/',
+      output: 15 },
+    { input: '/escaped \\/ slash/',
+      output: 0 }
+];
+const testsFindMatchingBrace = [
+    { input: '[square brace]',
+      output: 0 },
+    { input: '(round brace)',
+      output: 0  },
+    { input: '([()][nesting!])',
+      output: 0 },
+    { input: '[we have "quoted [" braces]',
+      output: 0 },
+    { input: '[we have /regex [/ braces]',
+      output: 0 },
+    { input: '([[])[] mismatched braces ]',
+      output: 1 }
+];
+const testsGetExpressionOffset = [
+    { input: 'abc.123',
+      output: 0 },
+    { input: 'foo().bar',
+      output: 0  },
+    { input: 'foo(bar',
+      output: 4 },
+    { input: 'foo[abc.match(/"/)]',
+      output: 0 }
+];
+const testsGetDeclaredConstants = [
+    { input: 'const foo = X; const bar = Y;',
+      output: ['foo', 'bar'] },
+    { input: 'const foo=X; const bar=Y',
+      output: ['foo', 'bar'] }
+];
+const testsIsUnsafeExpression = [
+    { input: 'foo.bar',
+      output: false },
+    { input: 'foo[\'bar\']',
+      output: false  },
+    { input: 'foo["a=b=c".match(/=/)',
+      output: false },
+    { input: 'foo[1==2]',
+      output: false },
+    { input: '(x=4)',
+      output: true },
+    { input: '(x = 4)',
+      output: true },
+    { input: '(x;y)',
+      output: true }
+];
+const testsModifyScope = [
+    "foo['a",
+    "foo()['b'",
+    "obj.foo()('a', 1, 2, 'b')().",
+    "foo.[.",
+    "foo]]]()))].",
+    "123'ab\"",
+    "Main.foo.bar = 3; bar.",
+    "(Main.foo = 3).",
+    "Main[Main.foo+=-1]."
+];
+
+
+
+// Utility function for comparing arrays
+function assertArrayEquals(errorMessage, array1, array2) {
+    JsUnit.assertEquals(errorMessage + ' length',
+                        array1.length, array2.length);
+    for (let j = 0; j < array1.length; j++) {
+        JsUnit.assertEquals(errorMessage + ' item ' + j,
+                            array1[j], array2[j]);
+    }
+}
+
+//
+// Test javascript parsing
+//
+
+for (let i = 0; i < testsFindMatchingQuote.length; i++) {
+    let text = testsFindMatchingQuote[i].input;
+    let match = JsParse.findMatchingQuote(text, text.length - 1);
+
+    JsUnit.assertEquals('Test testsFindMatchingQuote ' + i,
+			match, testsFindMatchingQuote[i].output);
+}
+
+for (let i = 0; i < testsFindMatchingSlash.length; i++) {
+    let text = testsFindMatchingSlash[i].input;
+    let match = JsParse.findMatchingSlash(text, text.length - 1);
+
+    JsUnit.assertEquals('Test testsFindMatchingSlash ' + i,
+			match, testsFindMatchingSlash[i].output);
+}
+
+for (let i = 0; i < testsFindMatchingBrace.length; i++) {
+    let text = testsFindMatchingBrace[i].input;
+    let match = JsParse.findMatchingBrace(text, text.length - 1);
+
+    JsUnit.assertEquals('Test testsFindMatchingBrace ' + i,
+			match, testsFindMatchingBrace[i].output);
+}
+
+for (let i = 0; i < testsGetExpressionOffset.length; i++) {
+    let text = testsGetExpressionOffset[i].input;
+    let match = JsParse.getExpressionOffset(text, text.length - 1);
+
+    JsUnit.assertEquals('Test testsGetExpressionOffset ' + i,
+			match, testsGetExpressionOffset[i].output);
+}
+
+for (let i = 0; i < testsGetDeclaredConstants.length; i++) {
+    let text = testsGetDeclaredConstants[i].input;
+    let match = JsParse.getDeclaredConstants(text);
+
+    assertArrayEquals('Test testsGetDeclaredConstants ' + i,
+		      match, testsGetDeclaredConstants[i].output);
+}
+
+for (let i = 0; i < testsIsUnsafeExpression.length; i++) {
+    let text = testsIsUnsafeExpression[i].input;
+    let unsafe = JsParse.isUnsafeExpression(text);
+
+    JsUnit.assertEquals('Test testsIsUnsafeExpression ' + i,
+			unsafe, testsIsUnsafeExpression[i].output);
+}
+
+//
+// Test safety of eval to get completions
+//
+
+for (let i = 0; i < testsModifyScope.length; i++) {
+    let text = testsModifyScope[i];
+    // We need to use var here for the with statement
+    var obj = {};
+
+    // Just as in JsParse.getCompletions, we will find the offset
+    // of the expression, test whether it is unsafe, and then eval it.
+    let offset = JsParse.getExpressionOffset(text, text.length - 1);
+    if (offset >= 0) {
+        text = text.slice(offset);
+
+        let matches = text.match(/(.*)\.(.*)/);
+        if (matches) {
+            [expr, base, attrHead] = matches;
+
+            if (!JsParse.isUnsafeExpression(base)) {
+                with (obj) {
+                    try {
+                        eval(HARNESS_COMMAND_HEADER + base);
+                    } catch (e) {
+                        JsUnit.assertNotEquals("Code '" + base + "' is valid code", e.constructor, SyntaxError);
+                    }
+                }
+           }
+       }
+    }
+    let propertyNames = Object.getOwnPropertyNames(obj);
+    JsUnit.assertEquals("The context '" + JSON.stringify(obj) + "' was not modified", propertyNames.length, 0);
+}



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