Skip to content

Commit

Permalink
Lint: add version consistency check (#3368)
Browse files Browse the repository at this point in the history
* Add test and fix for consistency

* Check against subfeatures supported but parent is null

* Add test and fix for consistency

* Remove fix scripts

* Remove redundant "file ok" message

* Remove redundant "file ok" message

* Switch to chalk for coloring (and use red instead of blue)

* Switch to chalk for coloring (and use red instead of blue)

* Re-remove fix scripts

* Remove redundant else clause

* Standardize initial error message

* Simplify module.exports

* Update consistency test to allow ranged versions

* Fix getVersionAdded to accept Boolean values again

* Revert "Fix getVersionAdded to accept Boolean values again"

This reverts commit 658f5c2.

* Fix missing space

* Fix logic

* Truly fix ranges by treating ranged versions as "1"

* Remove unused variable

* Ignore anything with a range in consistency test

* Move new test script to accommodate with new folder structure

* Remove unused variables

* Reword output messages

* Cleanup chalk templating; use nested bold statements

* Separate subfeatures list into separate forEach loop

* Show value and full path of subfeatures
  • Loading branch information
queengooborg authored and Elchi3 committed Oct 24, 2019
1 parent df9432f commit 112930d
Show file tree
Hide file tree
Showing 3 changed files with 288 additions and 0 deletions.
5 changes: 5 additions & 0 deletions test/lint.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const {
testStyle,
testSchema,
testVersions,
testConsistency,
testDescriptions
} = require('./linter/index.js');
const { IS_CI } = require('./utils.js')
Expand Down Expand Up @@ -54,6 +55,7 @@ function load(...files) {
hasLinkErrors = false,
hasBrowserErrors = false,
hasVersionErrors = false,
hasConsistencyErrors = false,
hasRealValueErrors = false,
hasPrefixErrors = false,
hasDescriptionsErrors = false;
Expand Down Expand Up @@ -89,6 +91,7 @@ function load(...files) {
hasLinkErrors = testLinks(file);
hasBrowserErrors = testBrowsers(file);
hasVersionErrors = testVersions(file);
hasConsistencyErrors = testConsistency(file);
hasRealValueErrors = testRealValues(file);
hasPrefixErrors = testPrefix(file);
hasDescriptionsErrors = testDescriptions(file);
Expand All @@ -105,6 +108,7 @@ function load(...files) {
hasLinkErrors,
hasBrowserErrors,
hasVersionErrors,
hasConsistencyErrors,
hasRealValueErrors,
hasPrefixErrors,
hasDescriptionsErrors
Expand Down Expand Up @@ -163,6 +167,7 @@ if (hasErrors) {
testVersions(file);
testRealValues(file);
testBrowsers(file);
testConsistency(file);
testPrefix(file);
testDescriptions(file);
}
Expand Down
2 changes: 2 additions & 0 deletions test/linter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const testRealValues = require('./test-real-values.js');
const testSchema = require('./test-schema.js');
const testStyle = require('./test-style.js');
const testVersions = require('./test-versions.js');
const testConsistency = require('./test-consistency.js');
const testDescriptions = require('./test-descriptions.js');

module.exports = {
Expand All @@ -16,5 +17,6 @@ module.exports = {
testStyle,
testSchema,
testVersions,
testConsistency,
testDescriptions
};
281 changes: 281 additions & 0 deletions test/linter/test-consistency.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
'use strict';
const path = require('path');
const compareVersions = require('compare-versions');
const chalk = require('chalk')

/**
* Consistency check.
*
* This checker aims at improving data quality
* by detecting inconsistent information.
*/
class ConsistencyChecker
{
/**
* @param {object} data
* @returns {Array<object>}
*/
check(data) {
return this.checkSubfeatures(data);
}

/**
* @param {object} data
* @param {array} path
* @returns {Array<object>}
*/
checkSubfeatures(data, path = []) {
let allErrors = [];

// Check this feature.
if (this.isFeature(data)) {
const feature = path.length ? path[path.length - 1] : 'ROOT';

const errors = this.checkFeature(data);

if (errors.length) {
allErrors.push({
feature,
path,
errors
});
}
}

// Check sub-features.
const keys = Object.keys(data).filter(key => key != '__compat');
keys.forEach(key => {
allErrors = [
...allErrors,
...this.checkSubfeatures(data[key], [...path, key])
];
});

return allErrors;
}

/**
* @param {object} data
* @returns {Array<object>}
*/
checkFeature(data) {
let errors = [];

const subfeatures = Object.keys(data).filter(key => this.isFeature(data[key]));

// Test whether sub-features are supported when basic support is not implemented
// For all unsupported browsers (basic support == false), sub-features should be set to false
const unsupportedInParent = this.extractUnsupportedBrowsers(data.__compat);
var inconsistentSubfeaturesByBrowser = {};

subfeatures.forEach(subfeature => {
const unsupportedInChild = this.extractUnsupportedBrowsers(data[subfeature].__compat);

const browsers = unsupportedInParent.filter(x => !unsupportedInChild.includes(x));

browsers.forEach(browser => {
inconsistentSubfeaturesByBrowser[browser] = inconsistentSubfeaturesByBrowser[browser] || [];
inconsistentSubfeaturesByBrowser[browser].push([subfeature, data[subfeature].__compat.support[browser].version_added]);
});
});

// Add errors
Object.keys(inconsistentSubfeaturesByBrowser).forEach(browser => {
const subfeatures = inconsistentSubfeaturesByBrowser[browser];
const errortype = 'unsupported';

errors.push({
errortype,
browser,
subfeatures
});
});

// Test whether sub-features are supported when basic support is not implemented
// For all unsupported browsers (basic support == false), sub-features should be set to false
const supportUnknownInParent = this.extractSupportUnknownBrowsers(data.__compat);
var inconsistentSubfeaturesByBrowser = {};

subfeatures.forEach(subfeature => {
const supportUnknownInChild = this.extractSupportNotTrueBrowsers(data[subfeature].__compat);

const browsers = supportUnknownInParent.filter(x => !supportUnknownInChild.includes(x));

browsers.forEach(browser => {
inconsistentSubfeaturesByBrowser[browser] = inconsistentSubfeaturesByBrowser[browser] || [];
inconsistentSubfeaturesByBrowser[browser].push([subfeature, data[subfeature].__compat.support[browser].version_added]);
});
});

// Add errors
Object.keys(inconsistentSubfeaturesByBrowser).forEach(browser => {
const subfeatures = inconsistentSubfeaturesByBrowser[browser];
const errortype = 'support_unknown';

errors.push({
errortype,
browser,
subfeatures
});
});

// Test whether sub-features are supported at an earlier version than basic support
const supportInParent = this.extractSupportedBrowsersWithVersion(data.__compat);
inconsistentSubfeaturesByBrowser = {};

subfeatures.forEach(subfeature => {
supportInParent.forEach(browser => {
if (data[subfeature].__compat.support[browser] != undefined && this.isVersionAddedGreater(data[subfeature].__compat.support[browser], data.__compat.support[browser])) {
inconsistentSubfeaturesByBrowser[browser] = inconsistentSubfeaturesByBrowser[browser] || [];
inconsistentSubfeaturesByBrowser[browser].push([subfeature, data[subfeature].__compat.support[browser].version_added]);
}
});
});

// Add errors
Object.keys(inconsistentSubfeaturesByBrowser).forEach(browser => {
const subfeatures = inconsistentSubfeaturesByBrowser[browser];
const errortype = 'subfeature_earlier_implementation';

errors.push({
errortype,
browser,
subfeatures
});
});

return errors;
}

/**
* @param {object} data
* @returns {boolean}
*/
isFeature(data) {
return '__compat' in data;
}

/**
* @param {object} compatData
* @returns {Array<string>}
*/
extractUnsupportedBrowsers(compatData) {
return this.extractBrowsers(compatData, data => data.version_added === false || typeof data.version_removed !== 'undefined' && data.version_removed !== false);
}

/**
* @param {object} compatData
* @returns {Array<string>}
*/
extractSupportUnknownBrowsers(compatData) {
return this.extractBrowsers(compatData, data => data.version_added === null);
}

/**
* @param {object} compatData
* @returns {Array<string>}
*/
extractSupportNotTrueBrowsers(compatData) {
return this.extractBrowsers(compatData, data => (data.version_added === false || data.version_added === null) || typeof data.version_removed !== 'undefined' && data.version_removed !== false);
}
/**
* @param {object} compatData
* @returns {Array<string>}
*/
extractSupportedBrowsersWithVersion(compatData) {
return this.extractBrowsers(compatData, data => typeof(data.version_added) === 'string');
}

/*
* @param {object} compatData
* @returns {string}
*/
getVersionAdded(compatData) {
var version_added = null;

if (typeof(compatData.version_added) === 'string')
return compatData.version_added;

if (compatData.constructor === Array) {
for (var i = compatData.length - 1; i >= 0; i--) {
var va = compatData[i].version_added;
if (typeof(va) === 'string' && (version_added == null || (typeof(version_added) === 'string' && compareVersions.compare(version_added.replace("≤", ""), va.replace("≤", ""), ">")))) {
version_added = va;
}
}
}

return version_added;
}

/*
* @param {string} a
* @param {string} b
* @returns {boolean}
*/
isVersionAddedGreater(a, b) {
var a_version_added = this.getVersionAdded(a);
var b_version_added = this.getVersionAdded(b);

if (typeof(a_version_added) === 'string' && typeof(b_version_added) === 'string') {
if (a_version_added.startsWith("≤") || b_version_added.startsWith("≤")) {
return false;
}
return compareVersions.compare(a_version_added, b_version_added, "<");
}

return false;
}

/**
*
* @param {object} compatData
* @param {callback} callback
* @returns {boolean}
*/
extractBrowsers(compatData, callback)
{
return Object.keys(compatData.support).filter(browser => {
const browserData = compatData.support[browser];

if (Array.isArray(browserData)) {
return browserData.every(callback);
} else if (typeof browserData === 'object') {
return callback(browserData);
} else {
return false;
}
});
}
}

function testConsistency(filename) {
let data = require(filename);

const checker = new ConsistencyChecker();
const errors = checker.check(data);

if (errors.length) {
console.error(chalk`{red Consistency - {bold ${errors.length} }${errors.length === 1 ? 'error' : 'errors'}:}`);
errors.forEach(({ feature, path, errors }) => {
console.error(chalk`{red → {bold ${errors.length}} × {bold ${feature}} [${path.join('.')}]: }`);
errors.forEach(({ errortype, browser, subfeatures }) => {
if (errortype == "unsupported") {
console.error(chalk`{red → No support in {bold ${browser}}, but support is declared in the following sub-feature(s):}`);
} else if (errortype == "support_unknown") {
console.error(chalk`{red → Unknown support in parent for {bold ${browser}}, but support is declared in the following sub-feature(s):}`);
} else if (errortype == "subfeature_earlier_implementation") {
console.error(chalk`{red → Basic support in {bold ${browser}} was declared implemented in a later version than the following sub-feature(s):}`);
}

subfeatures.forEach(subfeature => {
console.error(chalk`{red → {bold ${path.join('.')}.${subfeature[0]}}: ${subfeature[1]}}`);
});
});
})
return true;
}
return false;
}

module.exports = testConsistency;

0 comments on commit 112930d

Please sign in to comment.