[gnome-shell] lookingGlass: Add tab-completion
- From: Jasper St. Pierre <jstpierre src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-shell] lookingGlass: Add tab-completion
- Date: Sat, 5 Nov 2011 17:05:23 +0000 (UTC)
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]