diff options
Diffstat (limited to 'devtools/server/actors/compatibility/lib/MDNCompatibility.js')
-rw-r--r-- | devtools/server/actors/compatibility/lib/MDNCompatibility.js | 327 |
1 files changed, 327 insertions, 0 deletions
diff --git a/devtools/server/actors/compatibility/lib/MDNCompatibility.js b/devtools/server/actors/compatibility/lib/MDNCompatibility.js new file mode 100644 index 0000000000..9975123103 --- /dev/null +++ b/devtools/server/actors/compatibility/lib/MDNCompatibility.js @@ -0,0 +1,327 @@ +/* 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 _SUPPORT_STATE_BROWSER_NOT_FOUND = "BROWSER_NOT_FOUND"; +const _SUPPORT_STATE_SUPPORTED = "SUPPORTED"; +const _SUPPORT_STATE_UNSUPPORTED = "UNSUPPORTED"; +const _SUPPORT_STATE_UNSUPPORTED_PREFIX_NEEDED = "UNSUPPORTED_PREFIX_NEEDED"; + +loader.lazyRequireGetter( + this, + "COMPATIBILITY_ISSUE_TYPE", + "resource://devtools/shared/constants.js", + true +); + +loader.lazyRequireGetter( + this, + ["getCompatNode", "getCompatTable"], + "resource://devtools/shared/compatibility/helpers.js", + true +); + +const PREFIX_REGEX = /^-\w+-/; + +/** + * A class with methods used to query the MDN compatibility data for CSS properties and + * HTML nodes and attributes for specific browsers and versions. + */ +class MDNCompatibility { + /** + * Constructor. + * + * @param {JSON} cssPropertiesCompatData + * JSON of the compat data for CSS properties. + * https://github.com/mdn/browser-compat-data/tree/master/css/properties + */ + constructor(cssPropertiesCompatData) { + this._cssPropertiesCompatData = cssPropertiesCompatData; + } + + /** + * Return the CSS related compatibility issues from given CSS declaration blocks. + * + * @param {Array} declarations + * CSS declarations to check. + * e.g. [{ name: "background-color", value: "lime" }, ...] + * @param {Array} browsers + * Restrict compatibility checks to these browsers and versions. + * e.g. [{ id: "firefox", name: "Firefox", version: "68" }, ...] + * @return {Array} issues + */ + getCSSDeclarationBlockIssues(declarations, browsers) { + const summaries = []; + for (const { name: property } of declarations) { + // Ignore CSS custom properties as any name is valid. + if (property.startsWith("--")) { + continue; + } + + summaries.push(this._getCSSPropertyCompatSummary(browsers, property)); + } + + // Classify to aliases summaries and normal summaries. + const { aliasSummaries, normalSummaries } = + this._classifyCSSCompatSummaries(summaries, browsers); + + // Finally, convert to CSS issues. + return this._toCSSIssues(normalSummaries.concat(aliasSummaries)); + } + + /** + * Classify the compatibility summaries that are able to get from + * `getCSSPropertyCompatSummary`. + * There are CSS properties that can specify the style with plural aliases such as + * `user-select`, aggregates those as the aliases summaries. + * + * @param {Array} summaries + * Assume the result of _getCSSPropertyCompatSummary(). + * @param {Array} browsers + * All browsers that to check + * e.g. [{ id: "firefox", name: "Firefox", version: "68" }, ...] + * @return Object + * { + * aliasSummaries: Array of alias summary, + * normalSummaries: Array of normal summary + * } + */ + _classifyCSSCompatSummaries(summaries, browsers) { + const aliasSummariesMap = new Map(); + const normalSummaries = summaries.filter(s => { + const { + database, + invalid, + terms, + unsupportedBrowsers, + prefixNeededBrowsers, + } = s; + + if (invalid) { + return true; + } + + const alias = this._getAlias(database, terms); + if (!alias) { + return true; + } + + if (!aliasSummariesMap.has(alias)) { + aliasSummariesMap.set( + alias, + Object.assign(s, { + property: alias, + aliases: [], + unsupportedBrowsers: browsers, + prefixNeededBrowsers: browsers, + }) + ); + } + + // Update alias summary. + const terminal = terms.pop(); + const aliasSummary = aliasSummariesMap.get(alias); + if (!aliasSummary.aliases.includes(terminal)) { + aliasSummary.aliases.push(terminal); + } + aliasSummary.unsupportedBrowsers = + aliasSummary.unsupportedBrowsers.filter(b => + unsupportedBrowsers.includes(b) + ); + aliasSummary.prefixNeededBrowsers = + aliasSummary.prefixNeededBrowsers.filter(b => + prefixNeededBrowsers.includes(b) + ); + return false; + }); + + const aliasSummaries = [...aliasSummariesMap.values()].map(s => { + s.prefixNeeded = s.prefixNeededBrowsers.length !== 0; + return s; + }); + + return { aliasSummaries, normalSummaries }; + } + + _getAlias(compatNode, terms) { + const targetNode = getCompatNode(compatNode, terms); + return targetNode ? targetNode._aliasOf : null; + } + + /** + * Return the compatibility summary of the terms. + * + * @param {Array} browsers + * All browsers that to check + * e.g. [{ id: "firefox", name: "Firefox", version: "68" }, ...] + * @param {Array} database + * MDN compatibility dataset where finds from + * @param {Array} terms + * The terms which is checked the compatibility summary from the + * database. The paremeters are passed as `rest parameters`. + * e.g. _getCompatSummary(browsers, database, "user-select", ...) + * @return {Object} + * { + * database: The passed database as a parameter, + * terms: The passed terms as a parameter, + * url: The link which indicates the spec in MDN, + * deprecated: true if the spec of terms is deprecated, + * experimental: true if the spec of terms is experimental, + * unsupportedBrowsers: Array of unsupported browsers, + * } + */ + _getCompatSummary(browsers, database, terms) { + const compatTable = getCompatTable(database, terms); + + if (!compatTable) { + return { invalid: true, unsupportedBrowsers: [] }; + } + + const unsupportedBrowsers = []; + const prefixNeededBrowsers = []; + + for (const browser of browsers) { + const state = this._getSupportState( + compatTable, + browser, + database, + terms + ); + + switch (state) { + case _SUPPORT_STATE_UNSUPPORTED_PREFIX_NEEDED: { + prefixNeededBrowsers.push(browser); + unsupportedBrowsers.push(browser); + break; + } + case _SUPPORT_STATE_UNSUPPORTED: { + unsupportedBrowsers.push(browser); + break; + } + } + } + + const { deprecated, experimental } = compatTable.status || {}; + + return { + database, + terms, + url: compatTable.mdn_url, + specUrl: compatTable.spec_url, + deprecated, + experimental, + unsupportedBrowsers, + prefixNeededBrowsers, + }; + } + + /** + * Return the compatibility summary of the CSS property. + * This function just adds `property` filed to the result of `_getCompatSummary`. + * + * @param {Array} browsers + * All browsers that to check + * e.g. [{ id: "firefox", name: "Firefox", version: "68" }, ...] + * @return {Object} compatibility summary + */ + _getCSSPropertyCompatSummary(browsers, property) { + const summary = this._getCompatSummary( + browsers, + this._cssPropertiesCompatData, + [property] + ); + return Object.assign(summary, { property }); + } + + _getSupportState(compatTable, browser, compatNode, terms) { + const supportList = compatTable.support[browser.id]; + if (!supportList) { + return _SUPPORT_STATE_BROWSER_NOT_FOUND; + } + + const version = parseFloat(browser.version); + const terminal = terms.at(-1); + const prefix = terminal.match(PREFIX_REGEX)?.[0]; + + let prefixNeeded = false; + for (const support of supportList) { + const { alternative_name: alternativeName, added, removed } = support; + + if ( + // added id true when feature is supported, but we don't know the version + (added === true || + // `null` and `undefined` is when we don't know if it's supported. + // Since we don't want to have false negative, we consider it as supported + added === null || + added === undefined || + // It was added on a previous version number + added <= version) && + // `added` is false when the property isn't supported + added !== false && + // `removed` is false when the feature wasn't removevd + (removed === false || + // `null` and `undefined` is when we don't know if it was removed. + // Since we don't want to have false negative, we consider it as supported + removed === null || + removed === undefined || + // It was removed, but on a later version, so it's still supported + version <= removed) + ) { + if (alternativeName) { + if (alternativeName === terminal) { + return _SUPPORT_STATE_SUPPORTED; + } + } else if ( + support.prefix === prefix || + // There are compat data that are defined with prefix like "-moz-binding". + // In this case, we don't have to check the prefix. + (prefix && !this._getAlias(compatNode, terms)) + ) { + return _SUPPORT_STATE_SUPPORTED; + } + + prefixNeeded = true; + } + } + + return prefixNeeded + ? _SUPPORT_STATE_UNSUPPORTED_PREFIX_NEEDED + : _SUPPORT_STATE_UNSUPPORTED; + } + + _hasIssue({ unsupportedBrowsers, deprecated, experimental, invalid }) { + // Don't apply as issue the invalid term which was not in the database. + return ( + !invalid && (unsupportedBrowsers.length || deprecated || experimental) + ); + } + + _toIssue(summary, type) { + const issue = Object.assign({}, summary, { type }); + delete issue.database; + delete issue.terms; + delete issue.prefixNeededBrowsers; + return issue; + } + + _toCSSIssues(summaries) { + const issues = []; + + for (const summary of summaries) { + if (!this._hasIssue(summary)) { + continue; + } + + const type = summary.aliases + ? COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY_ALIASES + : COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY; + issues.push(this._toIssue(summary, type)); + } + + return issues; + } +} + +module.exports = MDNCompatibility; |