Skip to content

Commit

Permalink
DevTools: improve identifier extraction in SourceMapNamesResolver
Browse files Browse the repository at this point in the history
Apart from extracting identifier names from SourceMap.name entries,
this patch attempts to extract variable names from a mapped source.

BUG=none
R=dgozman, pfeldman_ooo

Review URL: https://codereview.chromium.org/1887913002

Cr-Commit-Position: refs/heads/master@{#387533}
  • Loading branch information
aslushnikov authored and Commit bot committed Apr 15, 2016
1 parent e2ca0ad commit decf4d4
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 64 deletions.
10 changes: 10 additions & 0 deletions front_end/common/Text.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,16 @@ WebInspector.TextCursor.prototype = {
return this._offset;
},

/**
* @param {number} offset
*/
resetTo: function(offset)
{
this._offset = offset;
this._lineNumber = this._lineEndings.lowerBound(offset);
this._columnNumber = this._lineNumber ? this._offset - this._lineEndings[this._lineNumber - 1] - 1 : this._offset;
},

/**
* @return {number}
*/
Expand Down
11 changes: 8 additions & 3 deletions front_end/es_tree/ESTreeWalker.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
/**
* @constructor
* @param {function(!ESTree.Node)} beforeVisit
* @param {function(!ESTree.Node)} afterVisit
* @param {function(!ESTree.Node)=} afterVisit
*/
WebInspector.ESTreeWalker = function(beforeVisit, afterVisit)
{
this._beforeVisit = beforeVisit;
this._afterVisit = afterVisit;
this._afterVisit = afterVisit || new Function();
}

WebInspector.ESTreeWalker.SkipSubtree = {};

WebInspector.ESTreeWalker.prototype = {
/**
* @param {!ESTree.Node} ast
Expand All @@ -32,7 +34,10 @@ WebInspector.ESTreeWalker.prototype = {
return;
node.parent = parent;

this._beforeVisit.call(null, node);
if (this._beforeVisit.call(null, node) === WebInspector.ESTreeWalker.SkipSubtree) {
this._afterVisit.call(null, node);
return;
}

var walkOrder = WebInspector.ESTreeWalker._walkOrder[node.type];
if (!walkOrder) {
Expand Down
2 changes: 2 additions & 0 deletions front_end/externs.js
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,8 @@ ESTree.Node = function()
this.name;
/** @type {(?ESTree.Node|undefined)} */
this.id;
/** @type {(number|undefined)} */
this.length;
}

/**
Expand Down
40 changes: 20 additions & 20 deletions front_end/formatter_worker/FormatterWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,49 +131,49 @@ WebInspector.evaluatableJavaScriptSubstring = function(content)
*/
WebInspector.javaScriptIdentifiers = function(content)
{
var root = acorn.parse(content, {});
var root = acorn.parse(content, { ranges: false, ecmaVersion: 6 });

/** @type {!Array<!ESTree.Node>} */
var identifiers = [];
var functionDeclarationCounter = 0;
var walker = new WebInspector.ESTreeWalker(beforeVisit, afterVisit);
var walker = new WebInspector.ESTreeWalker(beforeVisit);

/**
* @param {!ESTree.Node} node
* @return {boolean}
*/
function isFunction(node)
{
return node.type === "FunctionDeclaration" || node.type === "FunctionExpression";
return node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression";
}

/**
* @param {!ESTree.Node} node
*/
function beforeVisit(node)
{
if (isFunction(node))
functionDeclarationCounter++;
if (isFunction(node)) {
if (node.id)
identifiers.push(node.id);
return WebInspector.ESTreeWalker.SkipSubtree;
}

if (functionDeclarationCounter > 1)
if (node.type !== "Identifier")
return;

if (isFunction(node) && node.params)
identifiers.pushAll(node.params);

if (node.type === "VariableDeclarator")
identifiers.push(/** @type {!ESTree.Node} */(node.id));
if (node.parent && node.parent.type === "MemberExpression" && node.parent.property === node && !node.parent.computed)
return;
identifiers.push(node);
}

/**
* @param {!ESTree.Node} node
*/
function afterVisit(node)
{
if (isFunction(node))
functionDeclarationCounter--;
if (!root || root.type !== "Program" || root.body.length !== 1 || !isFunction(root.body[0])) {
postMessage([]);
return;
}

walker.walk(root);
var functionNode = root.body[0];
for (var param of functionNode.params)
walker.walk(param);
walker.walk(functionNode.body);
var reduced = identifiers.map(id => ({name: id.name, offset: id.start}));
postMessage(reduced);
}
Expand Down
9 changes: 8 additions & 1 deletion front_end/sdk/DebuggerModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -1038,7 +1038,6 @@ WebInspector.DebuggerModel.CallFrame.fromPayloadArray = function(debuggerModel,
}

WebInspector.DebuggerModel.CallFrame.prototype = {

/**
* @return {!WebInspector.Script}
*/
Expand Down Expand Up @@ -1205,6 +1204,14 @@ WebInspector.DebuggerModel.Scope = function(callFrame, ordinal)
}

WebInspector.DebuggerModel.Scope.prototype = {
/**
* @return {!WebInspector.DebuggerModel.CallFrame}
*/
callFrame: function()
{
return this._callFrame;
},

/**
* @return {string}
*/
Expand Down
172 changes: 132 additions & 40 deletions front_end/sources/SourceMapNamesResolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,53 +5,49 @@
WebInspector.SourceMapNamesResolver = {};

WebInspector.SourceMapNamesResolver._cachedMapSymbol = Symbol("cache");
WebInspector.SourceMapNamesResolver._cachedPromiseSymbol = Symbol("cachePromise");
WebInspector.SourceMapNamesResolver._cachedIdentifiersSymbol = Symbol("cachedIdentifiers");

/**
* @param {!WebInspector.DebuggerModel.Scope} scope
* @return {!Promise.<!Map<string, string>>}
* @constructor
* @param {string} name
* @param {number} lineNumber
* @param {number} columnNumber
*/
WebInspector.SourceMapNamesResolver._resolveScope = function(scope)
WebInspector.SourceMapNamesResolver.Identifier = function(name, lineNumber, columnNumber)
{
var cachedMap = scope[WebInspector.SourceMapNamesResolver._cachedMapSymbol];
if (cachedMap)
return Promise.resolve(cachedMap);

var cachedPromise = scope[WebInspector.SourceMapNamesResolver._cachedPromiseSymbol];
if (cachedPromise)
return cachedPromise;
this.name = name;
this.lineNumber = lineNumber;
this.columnNumber = columnNumber;
}

/**
* @param {!WebInspector.DebuggerModel.Scope} scope
* @return {!Promise<!Array<!WebInspector.SourceMapNamesResolver.Identifier>>}
*/
WebInspector.SourceMapNamesResolver._scopeIdentifiers = function(scope)
{
var startLocation = scope.startLocation();
var endLocation = scope.endLocation();

if (scope.type() === DebuggerAgent.ScopeType.Global || !startLocation || !endLocation || !startLocation.script().sourceMapURL || (startLocation.script() !== endLocation.script()))
return Promise.resolve(new Map());
return Promise.resolve(/** @type {!Array<!WebInspector.SourceMapNamesResolver.Identifier>}*/([]));

var script = startLocation.script();
var sourceMap = WebInspector.debuggerWorkspaceBinding.sourceMapForScript(script);
if (!sourceMap)
return Promise.resolve(new Map());

var promise = script.requestContent().then(onContent);
scope[WebInspector.SourceMapNamesResolver._cachedPromiseSymbol] = promise;
return promise;
return script.requestContent().then(onContent);

/**
* @param {?string} content
* @return {!Promise<!Map<string, string>>}
* @return {!Promise<!Array<!WebInspector.SourceMapNamesResolver.Identifier>>}
*/
function onContent(content)
{
if (!content)
return Promise.resolve(new Map());

var startLocation = scope.startLocation();
var endLocation = scope.endLocation();
var textRange = new WebInspector.TextRange(startLocation.lineNumber, startLocation.columnNumber, endLocation.lineNumber, endLocation.columnNumber);
return Promise.resolve(/** @type {!Array<!WebInspector.SourceMapNamesResolver.Identifier>}*/([]));

var text = new WebInspector.Text(content);
var scopeText = text.extract(textRange);
var scopeStart = text.toSourceRange(textRange).offset;
var scopeRange = new WebInspector.TextRange(startLocation.lineNumber, startLocation.columnNumber, endLocation.lineNumber, endLocation.columnNumber)
var scopeText = text.extract(scopeRange);
var scopeStart = text.toSourceRange(scopeRange).offset;
var prefix = "function fui";

return WebInspector.SourceMapNamesResolverWorker._instance().javaScriptIdentifiers(prefix + scopeText)
Expand All @@ -63,28 +59,124 @@ WebInspector.SourceMapNamesResolver._resolveScope = function(scope)
* @param {number} scopeStart
* @param {string} prefix
* @param {!Array<!{name: string, offset: number}>} identifiers
* @return {!Map<string, string>}
* @return {!Array<!WebInspector.SourceMapNamesResolver.Identifier>}
*/
function onIdentifiers(text, scopeStart, prefix, identifiers)
{
var namesMapping = new Map();
var lineEndings = text.lineEndings();

var result = [];
var cursor = new WebInspector.TextCursor(text.lineEndings());
var promises = [];
for (var i = 0; i < identifiers.length; ++i) {
var id = identifiers[i];
if (id.offset < prefix.length)
continue;
var start = scopeStart + id.offset - prefix.length;
cursor.resetTo(start);
result.push(new WebInspector.SourceMapNamesResolver.Identifier(id.name, cursor.lineNumber(), cursor.columnNumber()));
}
return result;
}
}

/**
* @param {!WebInspector.DebuggerModel.Scope} scope
* @return {!Promise.<!Map<string, string>>}
*/
WebInspector.SourceMapNamesResolver._resolveScope = function(scope)
{
var identifiersPromise = scope[WebInspector.SourceMapNamesResolver._cachedIdentifiersSymbol];
if (identifiersPromise)
return identifiersPromise;

var script = scope.callFrame().script;
var sourceMap = WebInspector.debuggerWorkspaceBinding.sourceMapForScript(script);
if (!sourceMap)
return Promise.resolve(new Map());

var lineNumber = lineEndings.lowerBound(start);
var columnNumber = start - (lineNumber === 0 ? 0 : (lineEndings[lineNumber - 1] + 1));
var entry = sourceMap.findEntry(lineNumber, columnNumber);
if (entry)
/** @type {!Map<string, !WebInspector.Text>} */
var textCache = new Map();
identifiersPromise = WebInspector.SourceMapNamesResolver._scopeIdentifiers(scope).then(onIdentifiers);
scope[WebInspector.SourceMapNamesResolver._cachedIdentifiersSymbol] = identifiersPromise;
return identifiersPromise;

/**
* @param {!Array<!WebInspector.SourceMapNamesResolver.Identifier>} identifiers
* @return {!Promise<!Map<string, string>>}
*/
function onIdentifiers(identifiers)
{
var namesMapping = new Map();
// Extract as much as possible from SourceMap.
for (var i = 0; i < identifiers.length; ++i) {
var id = identifiers[i];
var entry = sourceMap.findEntry(id.lineNumber, id.columnNumber);
if (entry && entry.name)
namesMapping.set(id.name, entry.name);
}

scope[WebInspector.SourceMapNamesResolver._cachedMapSymbol] = namesMapping;
delete scope[WebInspector.SourceMapNamesResolver._cachedPromiseSymbol];
WebInspector.SourceMapNamesResolver._scopeResolvedForTest();
return namesMapping;
// Resolve missing identifier names from sourcemap ranges.
var promises = [];
for (var i = 0; i < identifiers.length; ++i) {
var id = identifiers[i];
if (namesMapping.has(id.name))
continue;
var promise = resolveSourceName(id).then(onSourceNameResolved.bind(null, namesMapping, id));
promises.push(promise);
}
return Promise.all(promises)
.then(() => WebInspector.SourceMapNamesResolver._scopeResolvedForTest())
.then(() => namesMapping)
}

/**
* @param {!Map<string, string>} namesMapping
* @param {!WebInspector.SourceMapNamesResolver.Identifier} id
* @param {?string} sourceName
*/
function onSourceNameResolved(namesMapping, id, sourceName)
{
if (!sourceName)
return;
namesMapping.set(id.name, sourceName);
}

/**
* @param {!WebInspector.SourceMapNamesResolver.Identifier} id
* @return {!Promise<?string>}
*/
function resolveSourceName(id)
{
var startEntry = sourceMap.findEntry(id.lineNumber, id.columnNumber);
var endEntry = sourceMap.findEntry(id.lineNumber, id.columnNumber + id.name.length);
if (!startEntry || !endEntry || !startEntry.sourceURL || startEntry.sourceURL !== endEntry.sourceURL
|| !startEntry.sourceLineNumber || !startEntry.sourceColumnNumber
|| !endEntry.sourceLineNumber || !endEntry.sourceColumnNumber)
return Promise.resolve(/** @type {?string} */(null));
var sourceTextRange = new WebInspector.TextRange(startEntry.sourceLineNumber, startEntry.sourceColumnNumber, endEntry.sourceLineNumber, endEntry.sourceColumnNumber);
var uiSourceCode = WebInspector.networkMapping.uiSourceCodeForScriptURL(startEntry.sourceURL, script);
if (!uiSourceCode)
return Promise.resolve(/** @type {?string} */(null));

return uiSourceCode.requestContent()
.then(onSourceContent.bind(null, sourceTextRange));
}

/**
* @param {!WebInspector.TextRange} sourceTextRange
* @param {?string} content
* @return {?string}
*/
function onSourceContent(sourceTextRange, content)
{
if (!content)
return null;
var text = textCache.get(content);
if (!text) {
text = new WebInspector.Text(content);
textCache.set(content, text);
}
var originalIdentifier = text.extract(sourceTextRange).trim();
return /[a-zA-Z0-9_$]+/.test(originalIdentifier) ? originalIdentifier : null;
}
}

Expand Down Expand Up @@ -151,7 +243,7 @@ WebInspector.SourceMapNamesResolver.resolveExpression = function(callFrame, orig
if (reverseMapping.has(originalText))
return Promise.resolve(reverseMapping.get(originalText) || "");

return WebInspector.SourceMapNamesResolver._resolveExpression(callFrame, uiSourceCode, lineNumber, startColumnNumber, endColumnNumber)
return WebInspector.SourceMapNamesResolver._resolveExpression(callFrame, uiSourceCode, lineNumber, startColumnNumber, endColumnNumber);
}
}

Expand Down

0 comments on commit decf4d4

Please sign in to comment.