blob: f8f4face142f0fdb50d4230256694d68ee5d2e01 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @unrestricted
* @implements {SDK.SDKModelObserver<!SDK.DebuggerModel>}
*/
Bindings.BlackboxManager = class {
/**
* @param {!Bindings.DebuggerWorkspaceBinding} debuggerWorkspaceBinding
*/
constructor(debuggerWorkspaceBinding) {
this._debuggerWorkspaceBinding = debuggerWorkspaceBinding;
SDK.targetManager.addModelListener(
SDK.DebuggerModel, SDK.DebuggerModel.Events.ParsedScriptSource, this._parsedScriptSource, this);
SDK.targetManager.addModelListener(
SDK.DebuggerModel, SDK.DebuggerModel.Events.GlobalObjectCleared, this._globalObjectCleared, this);
Common.moduleSetting('skipStackFramesPattern').addChangeListener(this._patternChanged.bind(this));
Common.moduleSetting('skipContentScripts').addChangeListener(this._patternChanged.bind(this));
/** @type {!Set<function()>} */
this._listeners = new Set();
/** @type {!Map<!SDK.DebuggerModel, !Map<string, !Array<!Protocol.Debugger.ScriptPosition>>>} */
this._debuggerModelData = new Map();
/** @type {!Map<string, boolean>} */
this._isBlackboxedURLCache = new Map();
SDK.targetManager.observeModels(SDK.DebuggerModel, this);
}
/**
* @param {function()} listener
*/
addChangeListener(listener) {
this._listeners.add(listener);
}
/**
* @param {function()} listener
*/
removeChangeListener(listener) {
this._listeners.delete(listener);
}
/**
* @override
* @param {!SDK.DebuggerModel} debuggerModel
*/
modelAdded(debuggerModel) {
this._setBlackboxPatterns(debuggerModel);
}
/**
* @override
* @param {!SDK.DebuggerModel} debuggerModel
*/
modelRemoved(debuggerModel) {
this._debuggerModelData.delete(debuggerModel);
this._isBlackboxedURLCache.clear();
}
/**
* @param {!SDK.DebuggerModel} debuggerModel
* @return {!Promise<boolean>}
*/
_setBlackboxPatterns(debuggerModel) {
const regexPatterns = Common.moduleSetting('skipStackFramesPattern').getAsArray();
const patterns = /** @type {!Array<string>} */ ([]);
for (const item of regexPatterns) {
if (!item.disabled && item.pattern)
patterns.push(item.pattern);
}
return debuggerModel.setBlackboxPatterns(patterns);
}
/**
* @param {!SDK.DebuggerModel.Location} location
* @return {boolean}
*/
isBlackboxedRawLocation(location) {
const script = location.script();
if (!script)
return false;
const positions = this._scriptPositions(script);
if (!positions)
return this._isBlackboxedScript(script);
const index = positions.lowerBound(location, comparator);
return !!(index % 2);
/**
* @param {!SDK.DebuggerModel.Location} a
* @param {!Protocol.Debugger.ScriptPosition} b
* @return {number}
*/
function comparator(a, b) {
if (a.lineNumber !== b.lineNumber)
return a.lineNumber - b.lineNumber;
return a.columnNumber - b.columnNumber;
}
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
* @return {boolean}
*/
isBlackboxedUISourceCode(uiSourceCode) {
const projectType = uiSourceCode.project().type();
const isContentScript = projectType === Workspace.projectTypes.ContentScripts;
if (isContentScript && Common.moduleSetting('skipContentScripts').get())
return true;
const url = this._uiSourceCodeURL(uiSourceCode);
return url ? this.isBlackboxedURL(url) : false;
}
/**
* @param {string} url
* @param {boolean=} isContentScript
* @return {boolean}
*/
isBlackboxedURL(url, isContentScript) {
if (this._isBlackboxedURLCache.has(url))
return !!this._isBlackboxedURLCache.get(url);
if (isContentScript && Common.moduleSetting('skipContentScripts').get())
return true;
const regex = Common.moduleSetting('skipStackFramesPattern').asRegExp();
const isBlackboxed = regex && regex.test(url);
this._isBlackboxedURLCache.set(url, isBlackboxed);
return isBlackboxed;
}
/**
* @param {!SDK.Script} script
* @param {?SDK.SourceMap} sourceMap
* @return {!Promise<undefined>}
*/
sourceMapLoaded(script, sourceMap) {
if (!sourceMap)
return Promise.resolve();
const previousScriptState = this._scriptPositions(script);
if (!previousScriptState)
return Promise.resolve();
const hasBlackboxedMappings = sourceMap.sourceURLs().some(url => this.isBlackboxedURL(url));
const mappings = hasBlackboxedMappings ? sourceMap.mappings().slice() : [];
if (!mappings.length) {
if (previousScriptState.length > 0)
return this._setScriptState(script, []);
return Promise.resolve();
}
mappings.sort(mappingComparator);
let currentBlackboxed = false;
let isBlackboxed = false;
const positions = [];
// If content in script file begin is not mapped and one or more ranges are blackboxed then blackbox it.
if (mappings[0].lineNumber !== 0 || mappings[0].columnNumber !== 0) {
positions.push({lineNumber: 0, columnNumber: 0});
currentBlackboxed = true;
}
for (const mapping of mappings) {
if (mapping.sourceURL && currentBlackboxed !== this.isBlackboxedURL(mapping.sourceURL)) {
positions.push({lineNumber: mapping.lineNumber, columnNumber: mapping.columnNumber});
currentBlackboxed = !currentBlackboxed;
}
isBlackboxed = currentBlackboxed || isBlackboxed;
}
return this._setScriptState(script, !isBlackboxed ? [] : positions);
/**
* @param {!SDK.SourceMapEntry} a
* @param {!SDK.SourceMapEntry} b
* @return {number}
*/
function mappingComparator(a, b) {
if (a.lineNumber !== b.lineNumber)
return a.lineNumber - b.lineNumber;
return a.columnNumber - b.columnNumber;
}
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
* @return {?string}
*/
_uiSourceCodeURL(uiSourceCode) {
return uiSourceCode.project().type() === Workspace.projectTypes.Debugger ? null : uiSourceCode.url();
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
* @return {boolean}
*/
canBlackboxUISourceCode(uiSourceCode) {
const url = this._uiSourceCodeURL(uiSourceCode);
return url ? !!this._urlToRegExpString(url) : false;
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
*/
blackboxUISourceCode(uiSourceCode) {
const url = this._uiSourceCodeURL(uiSourceCode);
if (url)
this._blackboxURL(url);
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
*/
unblackboxUISourceCode(uiSourceCode) {
const url = this._uiSourceCodeURL(uiSourceCode);
if (url)
this._unblackboxURL(url);
}
blackboxContentScripts() {
Common.moduleSetting('skipContentScripts').set(true);
}
unblackboxContentScripts() {
Common.moduleSetting('skipContentScripts').set(false);
}
/**
* @param {string} url
*/
_blackboxURL(url) {
const regexPatterns = Common.moduleSetting('skipStackFramesPattern').getAsArray();
const regexValue = this._urlToRegExpString(url);
if (!regexValue)
return;
let found = false;
for (let i = 0; i < regexPatterns.length; ++i) {
const item = regexPatterns[i];
if (item.pattern === regexValue) {
item.disabled = false;
found = true;
break;
}
}
if (!found)
regexPatterns.push({pattern: regexValue});
Common.moduleSetting('skipStackFramesPattern').setAsArray(regexPatterns);
}
/**
* @param {string} url
*/
_unblackboxURL(url) {
let regexPatterns = Common.moduleSetting('skipStackFramesPattern').getAsArray();
const regexValue = Bindings.blackboxManager._urlToRegExpString(url);
if (!regexValue)
return;
regexPatterns = regexPatterns.filter(function(item) {
return item.pattern !== regexValue;
});
for (let i = 0; i < regexPatterns.length; ++i) {
const item = regexPatterns[i];
if (item.disabled)
continue;
try {
const regex = new RegExp(item.pattern);
if (regex.test(url))
item.disabled = true;
} catch (e) {
}
}
Common.moduleSetting('skipStackFramesPattern').setAsArray(regexPatterns);
}
_patternChanged() {
this._isBlackboxedURLCache.clear();
/** @type {!Array<!Promise>} */
const promises = [];
for (const debuggerModel of SDK.targetManager.models(SDK.DebuggerModel)) {
promises.push(this._setBlackboxPatterns(debuggerModel));
for (const script of debuggerModel.scripts())
promises.push(this._addScript(script).then(loadSourceMap.bind(this, script)));
}
Promise.all(promises).then(() => {
const listeners = Array.from(this._listeners);
for (const listener of listeners)
listener();
this._patternChangeFinishedForTests();
});
/**
* @param {!SDK.Script} script
* @return {!Promise<undefined>}
* @this {Bindings.BlackboxManager}
*/
function loadSourceMap(script) {
return this.sourceMapLoaded(script, this._debuggerWorkspaceBinding.sourceMapForScript(script));
}
}
_patternChangeFinishedForTests() {
// This method is sniffed in tests.
}
/**
* @param {!Common.Event} event
*/
_globalObjectCleared(event) {
const debuggerModel = /** @type {!SDK.DebuggerModel} */ (event.data);
this._debuggerModelData.delete(debuggerModel);
this._isBlackboxedURLCache.clear();
}
/**
* @param {!Common.Event} event
*/
_parsedScriptSource(event) {
const script = /** @type {!SDK.Script} */ (event.data);
this._addScript(script);
}
/**
* @param {!SDK.Script} script
* @return {!Promise<undefined>}
*/
_addScript(script) {
if (!script.sourceURL && !script.sourceMapURL)
return Promise.resolve();
const blackboxed = this._isBlackboxedScript(script);
return this._setScriptState(script, blackboxed ? [{lineNumber: 0, columnNumber: 0}] : []);
}
/**
* @param {!SDK.Script} script
* @return {boolean}
*/
_isBlackboxedScript(script) {
return this.isBlackboxedURL(script.sourceURL, script.isContentScript());
}
/**
* @param {!SDK.Script} script
* @return {?Array<!Protocol.Debugger.ScriptPosition>}
*/
_scriptPositions(script) {
if (this._debuggerModelData.has(script.debuggerModel))
return this._debuggerModelData.get(script.debuggerModel).get(script.scriptId) || null;
return null;
}
/**
* @param {!SDK.Script} script
* @param {!Array<!Protocol.Debugger.ScriptPosition>} positions
*/
_setScriptPositions(script, positions) {
const debuggerModel = script.debuggerModel;
if (!this._debuggerModelData.has(debuggerModel))
this._debuggerModelData.set(debuggerModel, new Map());
this._debuggerModelData.get(debuggerModel).set(script.scriptId, positions);
}
/**
* @param {!SDK.Script} script
* @param {!Array<!Protocol.Debugger.ScriptPosition>} positions
* @return {!Promise<undefined>}
*/
_setScriptState(script, positions) {
const previousScriptState = this._scriptPositions(script);
if (previousScriptState) {
let hasChanged = false;
hasChanged = previousScriptState.length !== positions.length;
for (let i = 0; !hasChanged && i < positions.length; ++i) {
hasChanged = positions[i].lineNumber !== previousScriptState[i].lineNumber ||
positions[i].columnNumber !== previousScriptState[i].columnNumber;
}
if (!hasChanged)
return Promise.resolve();
} else {
if (positions.length === 0)
return Promise.resolve().then(updateState.bind(this, false));
}
return script.setBlackboxedRanges(positions).then(updateState.bind(this));
/**
* @param {boolean} success
* @this {Bindings.BlackboxManager}
*/
function updateState(success) {
if (success) {
this._setScriptPositions(script, positions);
this._debuggerWorkspaceBinding.updateLocations(script);
const isBlackboxed = positions.length !== 0;
if (!isBlackboxed && script.sourceMapURL)
this._debuggerWorkspaceBinding.maybeLoadSourceMap(script);
} else {
const hasPositions = !!this._scriptPositions(script);
if (!hasPositions)
this._setScriptPositions(script, []);
}
}
}
/**
* @param {string} url
* @return {string}
*/
_urlToRegExpString(url) {
const parsedURL = new Common.ParsedURL(url);
if (parsedURL.isAboutBlank() || parsedURL.isDataURL())
return '';
if (!parsedURL.isValid)
return '^' + url.escapeForRegExp() + '$';
let name = parsedURL.lastPathComponent;
if (name)
name = '/' + name;
else if (parsedURL.folderPathComponents)
name = parsedURL.folderPathComponents + '/';
if (!name)
name = parsedURL.host;
if (!name)
return '';
const scheme = parsedURL.scheme;
let prefix = '';
if (scheme && scheme !== 'http' && scheme !== 'https') {
prefix = '^' + scheme + '://';
if (scheme === 'chrome-extension')
prefix += parsedURL.host + '\\b';
prefix += '.*';
}
return prefix + name.escapeForRegExp() + (url.endsWith(name) ? '$' : '\\b');
}
};
/** @type {!Bindings.BlackboxManager} */
Bindings.blackboxManager;