From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- devtools/server/actors/csscoverage.js | 726 ++++++++++++++++++++++++++++++++++ 1 file changed, 726 insertions(+) create mode 100644 devtools/server/actors/csscoverage.js (limited to 'devtools/server/actors/csscoverage.js') diff --git a/devtools/server/actors/csscoverage.js b/devtools/server/actors/csscoverage.js new file mode 100644 index 000000000..2f700656f --- /dev/null +++ b/devtools/server/actors/csscoverage.js @@ -0,0 +1,726 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { Cc, Ci } = require("chrome"); + +const Services = require("Services"); +const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm"); + +const events = require("sdk/event/core"); +const protocol = require("devtools/shared/protocol"); +const { cssUsageSpec } = require("devtools/shared/specs/csscoverage"); + +loader.lazyGetter(this, "DOMUtils", () => { + return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); +}); +loader.lazyRequireGetter(this, "stylesheets", "devtools/server/actors/stylesheets"); +loader.lazyRequireGetter(this, "prettifyCSS", "devtools/shared/inspector/css-logic", true); + +const CSSRule = Ci.nsIDOMCSSRule; + +const MAX_UNUSED_RULES = 10000; + +/** + * Allow: let foo = l10n.lookup("csscoverageFoo"); + */ +const l10n = exports.l10n = { + _URI: "chrome://devtools-shared/locale/csscoverage.properties", + lookup: function (msg) { + if (this._stringBundle == null) { + this._stringBundle = Services.strings.createBundle(this._URI); + } + return this._stringBundle.GetStringFromName(msg); + } +}; + +/** + * CSSUsage manages the collection of CSS usage data. + * The core of a CSSUsage is a JSON-able data structure called _knownRules + * which looks like this: + * This records the CSSStyleRules and their usage. + * The format is: + * Map({ + * ||: { + * selectorText: , + * test: , + * cssText: , + * isUsed: , + * presentOn: Set([ , ... ]), + * preLoadOn: Set([ , ... ]), + * isError: , + * } + * }) + * + * For example: + * this._knownRules = Map({ + * "http://eg.com/styles1.css|15|0": { + * selectorText: "p.quote:hover", + * test: "p.quote", + * cssText: "p.quote { color: red; }", + * isUsed: true, + * presentOn: Set([ "http://eg.com/page1.html", ... ]), + * preLoadOn: Set([ "http://eg.com/page1.html" ]), + * isError: false, + * }, ... + * }); + */ +var CSSUsageActor = protocol.ActorClassWithSpec(cssUsageSpec, { + initialize: function (conn, tabActor) { + protocol.Actor.prototype.initialize.call(this, conn); + + this._tabActor = tabActor; + this._running = false; + + this._onTabLoad = this._onTabLoad.bind(this); + this._onChange = this._onChange.bind(this); + + this._notifyOn = Ci.nsIWebProgress.NOTIFY_STATUS | + Ci.nsIWebProgress.NOTIFY_STATE_ALL; + }, + + destroy: function () { + this._tabActor = undefined; + + delete this._onTabLoad; + delete this._onChange; + + protocol.Actor.prototype.destroy.call(this); + }, + + /** + * Begin recording usage data + * @param noreload It's best if we start by reloading the current page + * because that starts the test at a known point, but there could be reasons + * why we don't want to do that (e.g. the page contains state that will be + * lost across a reload) + */ + start: function (noreload) { + if (this._running) { + throw new Error(l10n.lookup("csscoverageRunningError")); + } + + this._isOneShot = false; + this._visitedPages = new Set(); + this._knownRules = new Map(); + this._running = true; + this._tooManyUnused = false; + + this._progressListener = { + QueryInterface: XPCOMUtils.generateQI([ Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference ]), + + onStateChange: (progress, request, flags, status) => { + let isStop = flags & Ci.nsIWebProgressListener.STATE_STOP; + let isWindow = flags & Ci.nsIWebProgressListener.STATE_IS_WINDOW; + + if (isStop && isWindow) { + this._onTabLoad(progress.DOMWindow.document); + } + }, + + onLocationChange: () => {}, + onProgressChange: () => {}, + onSecurityChange: () => {}, + onStatusChange: () => {}, + destroy: () => {} + }; + + this._progress = this._tabActor.docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress); + this._progress.addProgressListener(this._progressListener, this._notifyOn); + + if (noreload) { + // If we're not starting by reloading the page, then pretend that onload + // has just happened. + this._onTabLoad(this._tabActor.window.document); + } else { + this._tabActor.window.location.reload(); + } + + events.emit(this, "state-change", { isRunning: true }); + }, + + /** + * Cease recording usage data + */ + stop: function () { + if (!this._running) { + throw new Error(l10n.lookup("csscoverageNotRunningError")); + } + + this._progress.removeProgressListener(this._progressListener, this._notifyOn); + this._progress = undefined; + + this._running = false; + events.emit(this, "state-change", { isRunning: false }); + }, + + /** + * Start/stop recording usage data depending on what we're currently doing. + */ + toggle: function () { + return this._running ? this.stop() : this.start(); + }, + + /** + * Running start() quickly followed by stop() does a bunch of unnecessary + * work, so this cuts all that out + */ + oneshot: function () { + if (this._running) { + throw new Error(l10n.lookup("csscoverageRunningError")); + } + + this._isOneShot = true; + this._visitedPages = new Set(); + this._knownRules = new Map(); + + this._populateKnownRules(this._tabActor.window.document); + this._updateUsage(this._tabActor.window.document, false); + }, + + /** + * Called by the ProgressListener to simulate a "load" event + */ + _onTabLoad: function (document) { + this._populateKnownRules(document); + this._updateUsage(document, true); + + this._observeMutations(document); + }, + + /** + * Setup a MutationObserver on the current document + */ + _observeMutations: function (document) { + let MutationObserver = document.defaultView.MutationObserver; + let observer = new MutationObserver(mutations => { + // It's possible that one of the mutations in this list adds a 'use' of + // a CSS rule, and another takes it away. See Bug 1010189 + this._onChange(document); + }); + + observer.observe(document, { + attributes: true, + childList: true, + characterData: false, + subtree: true + }); + }, + + /** + * Event handler for whenever we think the page has changed in a way that + * means the CSS usage might have changed. + */ + _onChange: function (document) { + // Ignore changes pre 'load' + if (!this._visitedPages.has(getURL(document))) { + return; + } + this._updateUsage(document, false); + }, + + /** + * Called whenever we think the list of stylesheets might have changed so + * we can update the list of rules that we should be checking + */ + _populateKnownRules: function (document) { + let url = getURL(document); + this._visitedPages.add(url); + // Go through all the rules in the current sheets adding them to knownRules + // if needed and adding the current url to the list of pages they're on + for (let rule of getAllSelectorRules(document)) { + let ruleId = ruleToId(rule); + let ruleData = this._knownRules.get(ruleId); + if (ruleData == null) { + ruleData = { + selectorText: rule.selectorText, + cssText: rule.cssText, + test: getTestSelector(rule.selectorText), + isUsed: false, + presentOn: new Set(), + preLoadOn: new Set(), + isError: false + }; + this._knownRules.set(ruleId, ruleData); + } + + ruleData.presentOn.add(url); + } + }, + + /** + * Update knownRules with usage information from the current page + */ + _updateUsage: function (document, isLoad) { + let qsaCount = 0; + + // Update this._data with matches to say 'used at load time' by sheet X + let url = getURL(document); + + for (let [ , ruleData ] of this._knownRules) { + // If it broke before, don't try again selectors don't change + if (ruleData.isError) { + continue; + } + + // If it's used somewhere already, don't bother checking again unless + // this is a load event in which case we need to add preLoadOn + if (!isLoad && ruleData.isUsed) { + continue; + } + + // Ignore rules that are not present on this page + if (!ruleData.presentOn.has(url)) { + continue; + } + + qsaCount++; + if (qsaCount > MAX_UNUSED_RULES) { + console.error("Too many unused rules on " + url + " "); + this._tooManyUnused = true; + continue; + } + + try { + let match = document.querySelector(ruleData.test); + if (match != null) { + ruleData.isUsed = true; + if (isLoad) { + ruleData.preLoadOn.add(url); + } + } + } catch (ex) { + ruleData.isError = true; + } + } + }, + + /** + * Returns a JSONable structure designed to help marking up the style editor, + * which describes the CSS selector usage. + * Example: + * [ + * { + * selectorText: "p#content", + * usage: "unused|used", + * start: { line: 3, column: 0 }, + * }, + * ... + * ] + */ + createEditorReport: function (url) { + if (this._knownRules == null) { + return { reports: [] }; + } + + let reports = []; + for (let [ruleId, ruleData] of this._knownRules) { + let { url: ruleUrl, line, column } = deconstructRuleId(ruleId); + if (ruleUrl !== url || ruleData.isUsed) { + continue; + } + + let ruleReport = { + selectorText: ruleData.selectorText, + start: { line: line, column: column } + }; + + if (ruleData.end) { + ruleReport.end = ruleData.end; + } + + reports.push(ruleReport); + } + + return { reports: reports }; + }, + + /** + * Compute the stylesheet URL and delegate the report creation to createEditorReport. + * See createEditorReport documentation. + * + * @param {StyleSheetActor} stylesheetActor + * the stylesheet actor for which the coverage report should be generated. + */ + createEditorReportForSheet: function (stylesheetActor) { + let url = sheetToUrl(stylesheetActor.rawSheet); + return this.createEditorReport(url); + }, + + /** + * Returns a JSONable structure designed for the page report which shows + * the recommended changes to a page. + * + * "preload" means that a rule is used before the load event happens, which + * means that the page could by optimized by placing it in a