diff --git a/common.gypi b/common.gypi index a3433e0ddc6bfa..812f6506c5ac46 100644 --- a/common.gypi +++ b/common.gypi @@ -27,7 +27,7 @@ # Reset this number to 0 on major V8 upgrades. # Increment by one for each non-official patch applied to deps/v8. - 'v8_embedder_string': '-node.3', + 'v8_embedder_string': '-node.4', # Enable disassembler for `--print-code` v8 options 'v8_enable_disassembler': 1, diff --git a/deps/v8/src/api.cc b/deps/v8/src/api.cc index 300b88f70eb701..f1bb82c72e7447 100644 --- a/deps/v8/src/api.cc +++ b/deps/v8/src/api.cc @@ -10172,6 +10172,25 @@ void debug::QueryObjects(v8::Local v8_context, predicate, objects); } +void debug::GlobalLexicalScopeNames( + v8::Local v8_context, + v8::PersistentValueVector* names) { + i::Handle context = Utils::OpenHandle(*v8_context); + i::Handle table( + context->global_object()->native_context()->script_context_table()); + for (int i = 0; i < table->used(); i++) { + i::Handle context = i::ScriptContextTable::GetContext(table, i); + DCHECK(context->IsScriptContext()); + i::Handle scope_info(context->scope_info()); + int local_count = scope_info->ContextLocalCount(); + for (int j = 0; j < local_count; ++j) { + i::String* name = scope_info->ContextLocalName(j); + if (i::ScopeInfo::VariableIsSynthetic(name)) continue; + names->Append(Utils::ToLocal(handle(name))); + } + } +} + Local CpuProfileNode::GetFunctionName() const { const i::ProfileNode* node = reinterpret_cast(this); i::Isolate* isolate = node->isolate(); diff --git a/deps/v8/src/debug/debug-interface.h b/deps/v8/src/debug/debug-interface.h index cc321ebfa23f7a..7063f1efe7585c 100644 --- a/deps/v8/src/debug/debug-interface.h +++ b/deps/v8/src/debug/debug-interface.h @@ -482,6 +482,9 @@ void QueryObjects(v8::Local context, QueryObjectPredicate* predicate, v8::PersistentValueVector* objects); +void GlobalLexicalScopeNames(v8::Local context, + v8::PersistentValueVector* names); + } // namespace debug } // namespace v8 diff --git a/deps/v8/src/inspector/js_protocol.json b/deps/v8/src/inspector/js_protocol.json index df7db67cdbd6fb..2d493e5b74687d 100644 --- a/deps/v8/src/inspector/js_protocol.json +++ b/deps/v8/src/inspector/js_protocol.json @@ -355,6 +355,17 @@ { "name": "objects", "$ref": "RemoteObject", "description": "Array with objects." } ], "experimental": true + }, + { + "name": "globalLexicalScopeNames", + "parameters": [ + { "name": "executionContextId", "$ref": "ExecutionContextId", "optional": true, "description": "Specifies in which execution context to lookup global scope variables." } + ], + "returns": [ + { "name": "names", "type": "array", "items": { "type": "string" } } + ], + "description": "Returns all let, const and class variables from global scope.", + "experimental": true } ], "events": [ diff --git a/deps/v8/src/inspector/v8-runtime-agent-impl.cc b/deps/v8/src/inspector/v8-runtime-agent-impl.cc index 8ecfbc57915b81..22d48e23bf27f8 100644 --- a/deps/v8/src/inspector/v8-runtime-agent-impl.cc +++ b/deps/v8/src/inspector/v8-runtime-agent-impl.cc @@ -586,6 +586,27 @@ Response V8RuntimeAgentImpl::queryObjects( resultArray, scope.objectGroupName(), false, false, objects); } +Response V8RuntimeAgentImpl::globalLexicalScopeNames( + Maybe executionContextId, + std::unique_ptr>* outNames) { + int contextId = 0; + Response response = ensureContext(m_inspector, m_session->contextGroupId(), + std::move(executionContextId), &contextId); + if (!response.isSuccess()) return response; + + InjectedScript::ContextScope scope(m_session, contextId); + response = scope.initialize(); + if (!response.isSuccess()) return response; + + v8::PersistentValueVector names(m_inspector->isolate()); + v8::debug::GlobalLexicalScopeNames(scope.context(), &names); + *outNames = protocol::Array::create(); + for (size_t i = 0; i < names.Size(); ++i) { + (*outNames)->addItem(toProtocolString(names.Get(i))); + } + return Response::OK(); +} + void V8RuntimeAgentImpl::restore() { if (!m_state->booleanProperty(V8RuntimeAgentImplState::runtimeEnabled, false)) return; diff --git a/deps/v8/src/inspector/v8-runtime-agent-impl.h b/deps/v8/src/inspector/v8-runtime-agent-impl.h index 1d5067b5602805..cc63b697c93765 100644 --- a/deps/v8/src/inspector/v8-runtime-agent-impl.h +++ b/deps/v8/src/inspector/v8-runtime-agent-impl.h @@ -101,6 +101,9 @@ class V8RuntimeAgentImpl : public protocol::Runtime::Backend { Response queryObjects( const String16& prototypeObjectId, std::unique_ptr* objects) override; + Response globalLexicalScopeNames( + Maybe executionContextId, + std::unique_ptr>* outNames) override; void reset(); void reportExecutionContextCreated(InspectedContext*); diff --git a/deps/v8/test/inspector/runtime/runtime-global-lexical-scope-names-expected.txt b/deps/v8/test/inspector/runtime/runtime-global-lexical-scope-names-expected.txt new file mode 100644 index 00000000000000..f24ac49ea02f45 --- /dev/null +++ b/deps/v8/test/inspector/runtime/runtime-global-lexical-scope-names-expected.txt @@ -0,0 +1,64 @@ +Test for Runtime.globalLexicalScopeVariablesNames +Running 'let a = 1' +Values: +a = 1 + +Running 'let b = 2' +Values: +a = 1 +b = 2 + +Running 'let b = 3' +Values: +a = 1 +b = 2 + +Running 'const c = 4' +Values: +a = 1 +b = 2 +c = 4 + +Running 'var d = 5' +(should not be in list of scoped variables) +Values: +a = 1 +b = 2 +c = 4 + +Running 'class Foo{}' +Values: +a = 1 +b = 2 +c = 4 +Foo = +{ + className : Function + description : class Foo{} + objectId : + type : function +} + +Adding script with scope variables +Values: +a = 1 +b = 2 +c = 4 +Foo = +{ + className : Function + description : class Foo{} + objectId : + type : function +} +e = 1 +f = 2 +g = 3 +Boo = +{ + className : Function + description : class Boo {} + objectId : + type : function +} + diff --git a/deps/v8/test/inspector/runtime/runtime-global-lexical-scope-names.js b/deps/v8/test/inspector/runtime/runtime-global-lexical-scope-names.js new file mode 100644 index 00000000000000..7e41f6a99f1a16 --- /dev/null +++ b/deps/v8/test/inspector/runtime/runtime-global-lexical-scope-names.js @@ -0,0 +1,59 @@ +// Copyright 2017 the V8 project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +let {session, contextGroup, Protocol} = + InspectorTest.start('Test for Runtime.globalLexicalScopeVariablesNames'); + +(async function test() { + InspectorTest.log('Running \'let a = 1\''); + Protocol.Runtime.evaluate({expression: 'let a = 1'}); + await dumpGlobalScopeVariables(); + + InspectorTest.log('Running \'let b = 2\''); + Protocol.Runtime.evaluate({expression: 'let b = 2'}); + await dumpGlobalScopeVariables(); + + InspectorTest.log('Running \'let b = 3\''); + Protocol.Runtime.evaluate({expression: 'let b = 3'}); + await dumpGlobalScopeVariables(); + + InspectorTest.log('Running \'const c = 4\''); + Protocol.Runtime.evaluate({expression: 'const c = 4'}); + await dumpGlobalScopeVariables(); + + InspectorTest.log('Running \'var d = 5\''); + InspectorTest.log('(should not be in list of scoped variables)'); + Protocol.Runtime.evaluate({expression: 'var d = 5'}); + await dumpGlobalScopeVariables(); + + InspectorTest.log('Running \'class Foo{}\''); + Protocol.Runtime.evaluate({expression: 'class Foo{}'}); + await dumpGlobalScopeVariables(); + + InspectorTest.log('Adding script with scope variables'); + contextGroup.addScript(` + let e = 1; + const f = 2; + const g = 3; + class Boo {}; + `); + await dumpGlobalScopeVariables(); + InspectorTest.completeTest(); +})(); + +async function dumpGlobalScopeVariables() { + let {result:{names}} = + await Protocol.Runtime.globalLexicalScopeNames(); + InspectorTest.log('Values:'); + for (let name of names) { + let {result:{result}} = await Protocol.Runtime.evaluate({expression: name}); + if (result.value) { + InspectorTest.log(`${name} = ${result.value}`); + } else { + InspectorTest.log(`${name} =`); + InspectorTest.logMessage(result); + } + } + InspectorTest.log(''); +} diff --git a/deps/v8/test/inspector/runtime/runtime-restore.js b/deps/v8/test/inspector/runtime/runtime-restore.js index 09e44677e5c13a..77fa823e7e8665 100644 --- a/deps/v8/test/inspector/runtime/runtime-restore.js +++ b/deps/v8/test/inspector/runtime/runtime-restore.js @@ -1,6 +1,6 @@ // Copyright 2017 the V8 project authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file.v8 +// found in the LICENSE file. let {session, contextGroup, Protocol} = InspectorTest.start('Checks that Runtime agent correctly restore its state.'); diff --git a/lib/internal/util/inspector.js b/lib/internal/util/inspector.js new file mode 100644 index 00000000000000..634d3302333584 --- /dev/null +++ b/lib/internal/util/inspector.js @@ -0,0 +1,25 @@ +'use strict'; + +const hasInspector = process.config.variables.v8_enable_inspector === 1; +const inspector = hasInspector ? require('inspector') : undefined; + +let session; + +function sendInspectorCommand(cb, onError) { + if (!hasInspector) return onError(); + if (session === undefined) session = new inspector.Session(); + try { + session.connect(); + try { + return cb(session); + } finally { + session.disconnect(); + } + } catch (e) { + return onError(); + } +} + +module.exports = { + sendInspectorCommand +}; diff --git a/lib/repl.js b/lib/repl.js index da3ed78e9ebab6..0378a6664c43e4 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -59,6 +59,7 @@ const Module = require('module'); const domain = require('domain'); const debug = util.debuglog('repl'); const errors = require('internal/errors'); +const { sendInspectorCommand } = require('internal/util/inspector'); const parentModule = module; const replMap = new WeakMap(); @@ -76,6 +77,7 @@ for (var n = 0; n < GLOBAL_OBJECT_PROPERTIES.length; n++) { GLOBAL_OBJECT_PROPERTIES[n]; } const kBufferedCommandSymbol = Symbol('bufferedCommand'); +const kContextId = Symbol('contextId'); try { // hack for require.resolve("./relative") to work properly. @@ -158,6 +160,8 @@ function REPLServer(prompt, self.last = undefined; self.breakEvalOnSigint = !!breakEvalOnSigint; self.editorMode = false; + // Context id for use with the inspector protocol. + self[kContextId] = undefined; // just for backwards compat, see github.com/joyent/node/pull/7127 self.rli = this; @@ -755,7 +759,16 @@ REPLServer.prototype.createContext = function() { if (this.useGlobal) { context = global; } else { - context = vm.createContext(); + sendInspectorCommand((session) => { + session.post('Runtime.enable'); + session.on('Runtime.executionContextCreated', ({ params }) => { + this[kContextId] = params.context.id; + }); + context = vm.createContext(); + session.post('Runtime.disable'); + }, () => { + context = vm.createContext(); + }); context.global = context; const _console = new Console(this.outputStream); Object.defineProperty(context, 'console', { @@ -890,6 +903,18 @@ function filteredOwnPropertyNames(obj) { return Object.getOwnPropertyNames(obj).filter(intFilter); } +function getGlobalLexicalScopeNames(contextId) { + return sendInspectorCommand((session) => { + let names = []; + session.post('Runtime.globalLexicalScopeNames', { + executionContextId: contextId + }, (error, result) => { + if (!error) names = result.names; + }); + return names; + }, () => []); +} + REPLServer.prototype.complete = function() { this.completer.apply(this, arguments); }; @@ -1053,6 +1078,7 @@ function complete(line, callback) { // If context is instance of vm.ScriptContext // Get global vars synchronously if (this.useGlobal || vm.isContext(this.context)) { + completionGroups.push(getGlobalLexicalScopeNames(this[kContextId])); var contextProto = this.context; while (contextProto = Object.getPrototypeOf(contextProto)) { completionGroups.push( diff --git a/node.gyp b/node.gyp index c1c83f6213f262..04d6eff57395b9 100644 --- a/node.gyp +++ b/node.gyp @@ -129,6 +129,7 @@ 'lib/internal/url.js', 'lib/internal/util.js', 'lib/internal/util/comparisons.js', + 'lib/internal/util/inspector.js', 'lib/internal/util/types.js', 'lib/internal/http2/core.js', 'lib/internal/http2/compat.js', diff --git a/test/parallel/test-repl-inspector.js b/test/parallel/test-repl-inspector.js new file mode 100644 index 00000000000000..b02f6139e72d60 --- /dev/null +++ b/test/parallel/test-repl-inspector.js @@ -0,0 +1,34 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const repl = require('repl'); + +common.skipIfInspectorDisabled(); + +// This test verifies that the V8 inspector API is usable in the REPL. + +const putIn = new common.ArrayStream(); +let output = ''; +putIn.write = function(data) { + output += data; +}; + +const testMe = repl.start('', putIn); + +putIn.run(['const myVariable = 42']); + +testMe.complete('myVar', common.mustCall((error, data) => { + assert.deepStrictEqual(data, [['myVariable'], 'myVar']); +})); + +putIn.run([ + 'const inspector = require("inspector")', + 'const session = new inspector.Session()', + 'session.connect()', + 'session.post("Runtime.evaluate", { expression: "1 + 1" }, console.log)', + 'session.disconnect()' +]); + +assert(output.includes( + "null { result: { type: 'number', value: 2, description: '2' } }")); diff --git a/test/parallel/test-repl-tab-complete.js b/test/parallel/test-repl-tab-complete.js index 4bf6b7209d0bb4..c9048d887d5cab 100644 --- a/test/parallel/test-repl-tab-complete.js +++ b/test/parallel/test-repl-tab-complete.js @@ -24,6 +24,7 @@ const common = require('../common'); const assert = require('assert'); const fixtures = require('../common/fixtures'); +const hasInspector = process.config.variables.v8_enable_inspector === 1; // We have to change the directory to ../fixtures before requiring repl // in order to make the tests for completion of node_modules work properly @@ -529,3 +530,23 @@ editorStream.run(['.editor']); editor.completer('var log = console.l', common.mustCall((error, data) => { assert.deepStrictEqual(data, [['console.log'], 'console.l']); })); + +{ + // tab completion of lexically scoped variables + const stream = new common.ArrayStream(); + const testRepl = repl.start({ stream }); + + stream.run([` + let lexicalLet = true; + const lexicalConst = true; + class lexicalKlass {} + `]); + + ['Let', 'Const', 'Klass'].forEach((type) => { + const query = `lexical${type[0]}`; + const expected = hasInspector ? [[`lexical${type}`], query] : []; + testRepl.complete(query, common.mustCall((error, data) => { + assert.deepStrictEqual(data, expected); + })); + }); +}