blob: 625409e82be6c5f1e63afc66056561b0f7c35303 [file] [edit]
/* This file is a part of @mdn/browser-compat-data
* See LICENSE file for more information. */
import { styleText } from 'node:util';
import { compareVersions } from 'compare-versions';
import { createStatementGroupKey } from '../utils.js';
import compareStatements from '../../scripts/lib/compare-statements.js';
/** @import {Logger} from '../utils.js' */
/** @import {BrowserName, SimpleSupportStatement, SupportStatement} from '../../types/types.js' */
/**
* Groups statements by group key.
* @param {SimpleSupportStatement[]} data The support statements to group.
* @returns {Map<string, SimpleSupportStatement[]>} the statement groups
*/
const groupByStatementKey = (data) => {
/** @type {Map<string, SimpleSupportStatement[]>} */
const groups = new Map();
for (const support of data) {
const key = createStatementGroupKey(support);
const group = groups.get(key);
if (group) {
group.push(support);
} else {
groups.set(key, [support]);
}
}
return groups;
};
/**
* Formats a support statement as a simplified JSON-like version range.
* @param {SimpleSupportStatement} support The statement to format
* @returns {string} The formatted range
*/
const formatRange = (support) => {
/** @type {string[]} */
const result = [];
if (support.version_added) {
result.push(`added: ${support.version_added}`);
}
if (support.version_removed) {
result.push(`removed: ${support.version_removed}`);
}
return `{ ${result.join(', ')} }`;
};
/**
* Process data and check to make sure there aren't support statements whose version ranges overlap.
* @param {SupportStatement} data The data to test
* @param {BrowserName} browser The name of the browser
* @param {object} options The check options
* @param {Logger} [options.logger] The logger to output errors to
* @param {boolean} [options.fix] Whether the statements should be fixed (if possible)
* @returns {SupportStatement} the data (with fixes, if specified)
*/
export const checkOverlap = (data, browser, { logger, fix = false }) => {
if (!Array.isArray(data)) {
// If there's only one statement, skip since this is a linter for multiple statements
return data;
}
const filteredData = data.filter((support) => !support.flags);
const groups = groupByStatementKey(filteredData);
for (const [groupKey, groupData] of groups.entries()) {
const statements = groupData.slice().sort(compareStatements).reverse();
for (let i = 0; i < statements.length - 1; i++) {
const current = /** @type {SimpleSupportStatement} */ (statements.at(i));
const next = /** @type {SimpleSupportStatement} */ (statements.at(i + 1));
if (!statementsOverlap(current, next)) {
continue;
}
let fixed = false;
if (fix) {
if (
typeof current.version_removed === 'undefined' &&
next.version_added !== false &&
next.version_added !== 'preview'
) {
current.version_removed = next.version_added;
fixed = true;
}
}
if (!fixed && logger) {
logger.error(
`${styleText('bold', browser)} statements overlap for ${styleText('bold', groupKey)}: ` +
`[${formatRange(next)}, ${formatRange(current)}]`,
);
}
}
}
return data;
};
/**
* Checks if the support statements overlap in terms of their version ranges.
* @param {SimpleSupportStatement} current the current statement.
* @param {SimpleSupportStatement} next the chronologically following statement.
* @returns {boolean} Whether the support statements overlap.
*/
const statementsOverlap = (current, next) => {
if (typeof current.version_removed === 'string') {
// If previous has no removed version, we always have an overlap.
if (next.version_added === 'preview') {
// Feature got re-introduced.
return false;
}
if (
typeof next.version_added === 'string' &&
compareVersions(current.version_removed, next.version_added) <= 0
) {
// No overlap.
return false;
}
} else if (
next.version_added === 'preview' &&
current.partial_implementation === true
) {
// Stable has partial support.
// Preview has full support.
return false;
}
return true;
};