From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- .../actors/utils/inactive-property-helper.js | 1163 ++++++++++++++++++++ 1 file changed, 1163 insertions(+) create mode 100644 devtools/server/actors/utils/inactive-property-helper.js (limited to 'devtools/server/actors/utils/inactive-property-helper.js') diff --git a/devtools/server/actors/utils/inactive-property-helper.js b/devtools/server/actors/utils/inactive-property-helper.js new file mode 100644 index 0000000000..253acc7b6e --- /dev/null +++ b/devtools/server/actors/utils/inactive-property-helper.js @@ -0,0 +1,1163 @@ +/* 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"; + +loader.lazyRequireGetter( + this, + "CssLogic", + "resource://devtools/server/actors/inspector/css-logic.js", + true +); + +const INACTIVE_CSS_ENABLED = Services.prefs.getBoolPref( + "devtools.inspector.inactive.css.enabled", + false +); + +const VISITED_MDN_LINK = "https://developer.mozilla.org/docs/Web/CSS/:visited"; +const VISITED_INVALID_PROPERTIES = allCssPropertiesExcept([ + "all", + "color", + "background", + "background-color", + "border", + "border-color", + "border-bottom-color", + "border-left-color", + "border-right-color", + "border-top-color", + "border-block", + "border-block-color", + "border-block-start-color", + "border-block-end-color", + "border-inline", + "border-inline-color", + "border-inline-start-color", + "border-inline-end-color", + "column-rule", + "column-rule-color", + "outline", + "outline-color", + "text-decoration-color", + "text-emphasis-color", +]); + +// Set of node names which are always treated as replaced elements: +const REPLACED_ELEMENTS_NAMES = new Set([ + "audio", + "br", + "button", + "canvas", + "embed", + "hr", + "iframe", + // Inputs are generally replaced elements. E.g. checkboxes and radios are replaced + // unless they have `appearance: none`. However unconditionally treating them + // as replaced is enough for our purpose here, and avoids extra complexity that + // will likely not be necessary in most cases. + "input", + "math", + "object", + "picture", + // Select is a replaced element if it has `size<=1` or no size specified, but + // unconditionally treating it as replaced is enough for our purpose here, and + // avoids extra complexity that will likely not be necessary in most cases. + "select", + "svg", + "textarea", + "video", +]); + +class InactivePropertyHelper { + /** + * A list of rules for when CSS properties have no effect. + * + * In certain situations, CSS properties do not have any effect. A common + * example is trying to set a width on an inline element like a . + * + * There are so many properties in CSS that it's difficult to remember which + * ones do and don't apply in certain situations. Some are straight-forward + * like `flex-wrap` only applying to an element that has `display:flex`. + * Others are less trivial like setting something other than a color on a + * `:visited` pseudo-class. + * + * This file contains "rules" in the form of objects with the following + * properties: + * { + * invalidProperties: + * Array of CSS property names that are inactive if the rule matches. + * when: + * The rule itself, a JS function used to identify the conditions + * indicating whether a property is valid or not. + * fixId: + * A Fluent id containing a suggested solution to the problem that is + * causing a property to be inactive. + * msgId: + * A Fluent id containing an error message explaining why a property is + * inactive in this situation. + * } + * + * If you add a new rule, also add a test for it in: + * server/tests/chrome/test_inspector-inactive-property-helper.html + * + * The main export is `isPropertyUsed()`, which can be used to check if a + * property is used or not, and why. + * + * NOTE: We should generally *not* add rules here for any CSS properties that + * inherit by default, because it's hard for us to know whether such + * properties are truly "inactive". Web developers might legitimately set + * such a property on any arbitrary element, in order to concisely establish + * the default property-value throughout that element's subtree. For example, + * consider the "list-style-*" properties, which inherit by default and which + * only have a rendering effect on elements with "display:list-item" + * (e.g.
  • ). It might superficially seem like we could add a rule here to + * warn about usages of these properties on non-"list-item" elements, but we + * shouldn't actually warn about that. A web developer may legitimately + * prefer to set these properties on an arbitrary container element (e.g. an + *
      element, or even the element) in order to concisely adjust the + * rendering of a whole list (or all the lists in a document). + */ + get VALIDATORS() { + return [ + // Flex container property used on non-flex container. + { + invalidProperties: ["flex-direction", "flex-flow", "flex-wrap"], + when: () => !this.flexContainer, + fixId: "inactive-css-not-flex-container-fix", + msgId: "inactive-css-not-flex-container", + }, + // Flex item property used on non-flex item. + { + invalidProperties: ["flex", "flex-basis", "flex-grow", "flex-shrink"], + when: () => !this.flexItem, + fixId: "inactive-css-not-flex-item-fix-2", + msgId: "inactive-css-not-flex-item", + }, + // Grid container property used on non-grid container. + { + invalidProperties: [ + "grid-auto-columns", + "grid-auto-flow", + "grid-auto-rows", + "grid-template", + "grid-template-areas", + "grid-template-columns", + "grid-template-rows", + "justify-items", + ], + when: () => !this.gridContainer, + fixId: "inactive-css-not-grid-container-fix", + msgId: "inactive-css-not-grid-container", + }, + // Grid item property used on non-grid item. + { + invalidProperties: [ + "grid-area", + "grid-column", + "grid-column-end", + "grid-column-start", + "grid-row", + "grid-row-end", + "grid-row-start", + "justify-self", + ], + when: () => !this.gridItem && !this.isAbsPosGridElement(), + fixId: "inactive-css-not-grid-item-fix-2", + msgId: "inactive-css-not-grid-item", + }, + // Grid and flex item properties used on non-grid or non-flex item. + { + invalidProperties: ["align-self", "place-self", "order"], + when: () => + !this.gridItem && !this.flexItem && !this.isAbsPosGridElement(), + fixId: "inactive-css-not-grid-or-flex-item-fix-3", + msgId: "inactive-css-not-grid-or-flex-item", + }, + // Grid and flex container properties used on non-grid or non-flex container. + { + invalidProperties: [ + "align-items", + "justify-content", + "place-content", + "place-items", + "row-gap", + // grid-*-gap are supported legacy shorthands for the corresponding *-gap properties. + // See https://drafts.csswg.org/css-align-3/#gap-legacy for more information. + "grid-row-gap", + ], + when: () => !this.gridContainer && !this.flexContainer, + fixId: "inactive-css-not-grid-or-flex-container-fix", + msgId: "inactive-css-not-grid-or-flex-container", + }, + // align-content is special as align-content:baseline does have an effect on all + // grid items, flex items and table cells, regardless of what type of box they are. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1598730 + { + invalidProperties: ["align-content"], + when: () => + !this.style["align-content"].includes("baseline") && + !this.gridContainer && + !this.flexContainer, + fixId: "inactive-css-not-grid-or-flex-container-fix", + msgId: "inactive-css-not-grid-or-flex-container", + }, + // column-gap and shorthands used on non-grid or non-flex or non-multi-col container. + { + invalidProperties: [ + "column-gap", + "gap", + "grid-gap", + // grid-*-gap are supported legacy shorthands for the corresponding *-gap properties. + // See https://drafts.csswg.org/css-align-3/#gap-legacy for more information. + "grid-column-gap", + ], + when: () => + !this.gridContainer && !this.flexContainer && !this.multiColContainer, + fixId: + "inactive-css-not-grid-or-flex-container-or-multicol-container-fix", + msgId: "inactive-css-not-grid-or-flex-container-or-multicol-container", + }, + // Inline properties used on non-inline-level elements. + { + invalidProperties: ["vertical-align"], + when: () => { + const { selectorText } = this.cssRule; + + const isFirstLetter = + selectorText && selectorText.includes("::first-letter"); + const isFirstLine = + selectorText && selectorText.includes("::first-line"); + + return !this.isInlineLevel() && !isFirstLetter && !isFirstLine; + }, + fixId: "inactive-css-not-inline-or-tablecell-fix", + msgId: "inactive-css-not-inline-or-tablecell", + }, + // (max-|min-)width used on inline elements, table rows, or row groups. + { + invalidProperties: ["max-width", "min-width", "width"], + when: () => + this.nonReplacedInlineBox || + this.horizontalTableTrack || + this.horizontalTableTrackGroup, + fixId: "inactive-css-non-replaced-inline-or-table-row-or-row-group-fix", + msgId: "inactive-css-property-because-of-display", + }, + // (max-|min-)height used on inline elements, table columns, or column groups. + { + invalidProperties: ["max-height", "min-height", "height"], + when: () => + this.nonReplacedInlineBox || + this.verticalTableTrack || + this.verticalTableTrackGroup, + fixId: + "inactive-css-non-replaced-inline-or-table-column-or-column-group-fix", + msgId: "inactive-css-property-because-of-display", + }, + { + invalidProperties: ["display"], + when: () => + this.isFloated && + this.checkResolvedStyle("display", [ + "inline", + "inline-block", + "inline-table", + "inline-flex", + "inline-grid", + "table-cell", + "table-row", + "table-row-group", + "table-header-group", + "table-footer-group", + "table-column", + "table-column-group", + "table-caption", + ]), + fixId: "inactive-css-not-display-block-on-floated-fix", + msgId: "inactive-css-not-display-block-on-floated", + }, + // The property is impossible to override due to :visited restriction. + { + invalidProperties: VISITED_INVALID_PROPERTIES, + when: () => this.isVisitedRule(), + fixId: "learn-more", + msgId: "inactive-css-property-is-impossible-to-override-in-visited", + learnMoreURL: VISITED_MDN_LINK, + }, + // top, right, bottom, left properties used on non positioned boxes. + { + invalidProperties: ["top", "right", "bottom", "left"], + when: () => !this.isPositioned, + fixId: "inactive-css-position-property-on-unpositioned-box-fix", + msgId: "inactive-css-position-property-on-unpositioned-box", + }, + // z-index property used on non positioned boxes that are not grid/flex items. + { + invalidProperties: ["z-index"], + when: () => !this.isPositioned && !this.gridItem && !this.flexItem, + fixId: "inactive-css-position-property-on-unpositioned-box-fix", + msgId: "inactive-css-position-property-on-unpositioned-box", + }, + // text-overflow property used on elements for which 'overflow' is set to 'visible' + // (the initial value) in the inline axis. Note that this validator only checks if + // 'overflow-inline' computes to 'visible' on the element. + // In theory, we should also be checking if the element is a block as this doesn't + // normally work on inline element. However there are many edge cases that made it + // impossible for the JS code to determine whether the type of box would support + // text-overflow. So, rather than risking to show invalid warnings, we decided to + // only warn when 'overflow-inline: visible' was set. There is more information + // about this in this discussion https://phabricator.services.mozilla.com/D62407 and + // on the bug https://bugzilla.mozilla.org/show_bug.cgi?id=1551578 + { + invalidProperties: ["text-overflow"], + when: () => this.checkComputedStyle("overflow-inline", ["visible"]), + fixId: "inactive-text-overflow-when-no-overflow-fix", + msgId: "inactive-text-overflow-when-no-overflow", + }, + // margin properties used on table internal elements. + { + invalidProperties: [ + "margin", + "margin-block", + "margin-block-end", + "margin-block-start", + "margin-bottom", + "margin-inline", + "margin-inline-end", + "margin-inline-start", + "margin-left", + "margin-right", + "margin-top", + ], + when: () => this.internalTableElement, + fixId: "inactive-css-not-for-internal-table-elements-fix", + msgId: "inactive-css-not-for-internal-table-elements", + }, + // padding properties used on table internal elements except table cells. + { + invalidProperties: [ + "padding", + "padding-block", + "padding-block-end", + "padding-block-start", + "padding-bottom", + "padding-inline", + "padding-inline-end", + "padding-inline-start", + "padding-left", + "padding-right", + "padding-top", + ], + when: () => + this.internalTableElement && + !this.checkComputedStyle("display", ["table-cell"]), + fixId: + "inactive-css-not-for-internal-table-elements-except-table-cells-fix", + msgId: + "inactive-css-not-for-internal-table-elements-except-table-cells", + }, + // table-layout used on non-table elements. + { + invalidProperties: ["table-layout"], + when: () => + !this.checkComputedStyle("display", ["table", "inline-table"]), + fixId: "inactive-css-not-table-fix", + msgId: "inactive-css-not-table", + }, + // scroll-padding-* properties used on non-scrollable elements. + { + invalidProperties: [ + "scroll-padding", + "scroll-padding-top", + "scroll-padding-right", + "scroll-padding-bottom", + "scroll-padding-left", + "scroll-padding-block", + "scroll-padding-block-end", + "scroll-padding-block-start", + "scroll-padding-inline", + "scroll-padding-inline-end", + "scroll-padding-inline-start", + ], + when: () => !this.isScrollContainer, + fixId: "inactive-scroll-padding-when-not-scroll-container-fix", + msgId: "inactive-scroll-padding-when-not-scroll-container", + }, + // border-image properties used on internal table with border collapse. + { + invalidProperties: [ + "border-image", + "border-image-outset", + "border-image-repeat", + "border-image-slice", + "border-image-source", + "border-image-width", + ], + when: () => + this.internalTableElement && + this.checkTableParentHasBorderCollapsed(), + fixId: "inactive-css-border-image-fix", + msgId: "inactive-css-border-image", + }, + // width & height properties used on ruby elements. + { + invalidProperties: [ + "height", + "min-height", + "max-height", + "width", + "min-width", + "max-width", + ], + when: () => this.checkComputedStyle("display", ["ruby", "ruby-text"]), + fixId: "inactive-css-ruby-element-fix", + msgId: "inactive-css-ruby-element", + }, + ]; + } + + /** + * Get a list of unique CSS property names for which there are checks + * for used/unused state. + * + * @return {Set} + * List of CSS properties + */ + get invalidProperties() { + if (!this._invalidProperties) { + const allProps = this.VALIDATORS.map(v => v.invalidProperties).flat(); + this._invalidProperties = new Set(allProps); + } + + return this._invalidProperties; + } + + /** + * Is this CSS property having any effect on this element? + * + * @param {DOMNode} el + * The DOM element. + * @param {Style} elStyle + * The computed style for this DOMNode. + * @param {DOMRule} cssRule + * The CSS rule the property is defined in. + * @param {String} property + * The CSS property name. + * + * @return {Object} object + * @return {String} object.display + * The element computed display value. + * @return {String} object.fixId + * A Fluent id containing a suggested solution to the problem that is + * causing a property to be inactive. + * @return {String} object.msgId + * A Fluent id containing an error message explaining why a property + * is inactive in this situation. + * @return {String} object.property + * The inactive property name. + * @return {String} object.learnMoreURL + * An optional link if we need to open an other link than + * the default MDN property one. + * @return {Boolean} object.used + * true if the property is used. + */ + isPropertyUsed(el, elStyle, cssRule, property) { + // Assume the property is used when: + // - the Inactive CSS pref is not enabled + // - the property is not in the list of properties to check + if (!INACTIVE_CSS_ENABLED || !this.invalidProperties.has(property)) { + return { used: true }; + } + + let fixId = ""; + let msgId = ""; + let learnMoreURL = null; + let used = true; + + this.VALIDATORS.some(validator => { + // First check if this rule cares about this property. + let isRuleConcerned = false; + + if (validator.invalidProperties) { + isRuleConcerned = validator.invalidProperties.includes(property); + } + + if (!isRuleConcerned) { + return false; + } + + this.select(el, elStyle, cssRule, property); + + // And then run the validator, gathering the error message if the + // validator passes. + if (validator.when()) { + fixId = validator.fixId; + msgId = validator.msgId; + learnMoreURL = validator.learnMoreURL; + used = false; + + return true; + } + + return false; + }); + + this.unselect(); + + // Accessing elStyle might throws, we wrap it in a try/catch block to avoid test + // failures. + let display; + try { + display = elStyle ? elStyle.display : null; + } catch (e) {} + + return { + display, + fixId, + msgId, + property, + learnMoreURL, + used, + }; + } + + /** + * Focus on a node. + * + * @param {DOMNode} node + * Node to focus on. + */ + select(node, style, cssRule, property) { + this._node = node; + this._cssRule = cssRule; + this._property = property; + this._style = style; + } + + /** + * Clear references to avoid leaks. + */ + unselect() { + this._node = null; + this._cssRule = null; + this._property = null; + this._style = null; + } + + /** + * Provide a public reference to node. + */ + get node() { + return this._node; + } + + /** + * Cache and provide node's computed style. + */ + get style() { + return this._style; + } + + /** + * Provide a public reference to the css rule. + */ + get cssRule() { + return this._cssRule; + } + + /** + * Check if the current node's propName is set to one of the values passed in + * the values array. + * + * @param {String} propName + * Property name to check. + * @param {Array} values + * Values to compare against. + */ + checkComputedStyle(propName, values) { + if (!this.style) { + return false; + } + return values.some(value => this.style[propName] === value); + } + + /** + * Check if a rule's propName is set to one of the values passed in the values + * array. + * + * @param {String} propName + * Property name to check. + * @param {Array} values + * Values to compare against. + */ + checkResolvedStyle(propName, values) { + if (!(this.cssRule && this.cssRule.style)) { + return false; + } + const { style } = this.cssRule; + + return values.some(value => style[propName] === value); + } + + /** + * Check if the current node is an inline-level box. + */ + isInlineLevel() { + return this.checkComputedStyle("display", [ + "inline", + "inline-block", + "inline-table", + "inline-flex", + "inline-grid", + "table-cell", + "table-row", + "table-row-group", + "table-header-group", + "table-footer-group", + ]); + } + + /** + * Check if the current node is a flex container i.e. a node that has a style + * of `display:flex` or `display:inline-flex`. + */ + get flexContainer() { + return this.checkComputedStyle("display", ["flex", "inline-flex"]); + } + + /** + * Check if the current node is a flex item. + */ + get flexItem() { + return this.isFlexItem(this.node); + } + + /** + * Check if the current node is a grid container i.e. a node that has a style + * of `display:grid` or `display:inline-grid`. + */ + get gridContainer() { + return this.checkComputedStyle("display", ["grid", "inline-grid"]); + } + + /** + * Check if the current node is a grid item. + */ + get gridItem() { + return this.isGridItem(this.node); + } + + /** + * Check if the current node is a multi-column container, i.e. a node element whose + * `column-width` or `column-count` property is not `auto`. + */ + get multiColContainer() { + const autoColumnWidth = this.checkComputedStyle("column-width", ["auto"]); + const autoColumnCount = this.checkComputedStyle("column-count", ["auto"]); + + return !autoColumnWidth || !autoColumnCount; + } + + /** + * Check if the current node is a table row. + */ + get tableRow() { + return this.style && this.style.display === "table-row"; + } + + /** + * Check if the current node is a table column. + */ + get tableColumn() { + return this.style && this.style.display === "table-column"; + } + + /** + * Check if the current node is an internal table element. + */ + get internalTableElement() { + return this.checkComputedStyle("display", [ + "table-cell", + "table-row", + "table-row-group", + "table-header-group", + "table-footer-group", + "table-column", + "table-column-group", + ]); + } + + /** + * Check if the current node is a horizontal table track. That is: either a table row + * displayed in horizontal writing mode, or a table column displayed in vertical writing + * mode. + */ + get horizontalTableTrack() { + if (!this.tableRow && !this.tableColumn) { + return false; + } + + const tableTrackParent = this.getTableTrackParent(); + + return this.hasVerticalWritingMode(tableTrackParent) + ? this.tableColumn + : this.tableRow; + } + + /** + * Check if the current node is a vertical table track. That is: either a table row + * displayed in vertical writing mode, or a table column displayed in horizontal writing + * mode. + */ + get verticalTableTrack() { + if (!this.tableRow && !this.tableColumn) { + return false; + } + + const tableTrackParent = this.getTableTrackParent(); + + return this.hasVerticalWritingMode(tableTrackParent) + ? this.tableRow + : this.tableColumn; + } + + /** + * Check if the current node is a row group. + */ + get rowGroup() { + return this.isRowGroup(this.node); + } + + /** + * Check if the current node is a table column group. + */ + get columnGroup() { + return this.isColumnGroup(this.node); + } + + /** + * Check if the current node is a horizontal table track group. That is: either a table + * row group displayed in horizontal writing mode, or a table column group displayed in + * vertical writing mode. + */ + get horizontalTableTrackGroup() { + if (!this.rowGroup && !this.columnGroup) { + return false; + } + + const tableTrackParent = this.getTableTrackParent(true); + const isVertical = this.hasVerticalWritingMode(tableTrackParent); + + const isHorizontalRowGroup = this.rowGroup && !isVertical; + const isHorizontalColumnGroup = this.columnGroup && isVertical; + + return isHorizontalRowGroup || isHorizontalColumnGroup; + } + + /** + * Check if the current node is a vertical table track group. That is: either a table row + * group displayed in vertical writing mode, or a table column group displayed in + * horizontal writing mode. + */ + get verticalTableTrackGroup() { + if (!this.rowGroup && !this.columnGroup) { + return false; + } + + const tableTrackParent = this.getTableTrackParent(true); + const isVertical = this.hasVerticalWritingMode(tableTrackParent); + + const isVerticalRowGroup = this.rowGroup && isVertical; + const isVerticalColumnGroup = this.columnGroup && !isVertical; + + return isVerticalRowGroup || isVerticalColumnGroup; + } + + /** + * Returns whether this element uses CSS layout. + */ + get hasCssLayout() { + return !this.isSvg && !this.isMathMl; + } + + /** + * Check if the current node is a non-replaced CSS inline box. + */ + get nonReplacedInlineBox() { + return ( + this.hasCssLayout && + this.nonReplaced && + this.style && + this.style.display === "inline" + ); + } + + /** + * Check if the current node is a non-replaced element. See `replaced()` for + * a description of what a replaced element is. + */ + get nonReplaced() { + return !this.replaced; + } + + /** + * Check if the current node is an absolutely-positioned element. + */ + get isAbsolutelyPositioned() { + return this.checkComputedStyle("position", ["absolute", "fixed"]); + } + + /** + * Check if the current node is positioned (i.e. its position property has a value other + * than static). + */ + get isPositioned() { + return this.checkComputedStyle("position", [ + "relative", + "absolute", + "fixed", + "sticky", + ]); + } + + /** + * Check if the current node is floated + */ + get isFloated() { + return this.style && this.style.cssFloat !== "none"; + } + + /** + * Check if the current node is scrollable + */ + get isScrollContainer() { + // If `overflow` doesn't contain the values `visible` or `clip`, it is a scroll container. + // While `hidden` doesn't allow scrolling via a user interaction, the element can + // still be scrolled programmatically. + // See https://www.w3.org/TR/css-overflow-3/#overflow-properties. + const overflow = computedStyle(this.node).overflow; + // `overflow` is a shorthand for `overflow-x` and `overflow-y` + // (and with that also for `overflow-inline` and `overflow-block`), + // so may hold two values. + const overflowValues = overflow.split(" "); + return !( + overflowValues.includes("visible") || overflowValues.includes("clip") + ); + } + + /** + * Check if the current node is a replaced element i.e. an element with + * content that will be replaced e.g. ,