diff --git a/src/messages/css.js b/src/messages/css.js index 78468c645f..6c33f1b8e4 100644 --- a/src/messages/css.js +++ b/src/messages/css.js @@ -11,5 +11,6 @@ export const CSS_SYNTAX_ERROR = { export const INVALID_SELECTOR_NESTING = { code: 'INVALID_SELECTOR_NESTING', message: i18n._('Invalid nesting of selectors found'), - description: i18n._(`Selectors should not be nested`), + description: i18n._(`Selector nesting is supported from firefox version 117.0 + and above`), }; diff --git a/src/parsers/manifestjson.js b/src/parsers/manifestjson.js index 32bd18f25b..7b4dae8f65 100644 --- a/src/parsers/manifestjson.js +++ b/src/parsers/manifestjson.js @@ -1285,6 +1285,19 @@ export default class ManifestJSONParser extends JSONParser { return apiPaths; } + /** + * @typedef {Object} Metadata + * @property {string} id + * @property {number} manifestVersion + * @property {string} name + * @property {number} type + * @property {string} version + * @property {string} firefoxMinVersion + * @property {string} firefoxStrictMinVersion + * @property {Set} experimentApiPaths + * + * @returns {Metadata} + */ getMetadata() { return { id: this.getAddonId(), @@ -1292,10 +1305,14 @@ export default class ManifestJSONParser extends JSONParser { name: this.parsedJSON.name, type: PACKAGE_EXTENSION, version: this.parsedJSON.version, + // This is the `strict_min_version` value set in the `manifest.json` file + // for Firefox for desktop. firefoxMinVersion: this.parsedJSON.applications && this.parsedJSON.applications.gecko && this.parsedJSON.applications.gecko.strict_min_version, + // This is the strict min *major* version for Firefox for desktop. + firefoxStrictMinVersion: firefoxStrictMinVersion(this.parsedJSON), experimentApiPaths: this.getExperimentApiPaths(), }; } diff --git a/src/rules/css/invalidNesting.js b/src/rules/css/invalidNesting.js index 9667249e8c..dcb111f136 100644 --- a/src/rules/css/invalidNesting.js +++ b/src/rules/css/invalidNesting.js @@ -1,10 +1,24 @@ import * as messages from 'messages'; +import { basicCompatVersionComparison } from '../../utils'; + +const CSS_NESTING_MIN_VERSION = 117; + export function invalidNesting( cssNode, filename, - { startLine, startColumn } = {} + { startLine, startColumn, addonMetadata } = {} ) { + if ( + addonMetadata?.firefoxStrictMinVersion && + !basicCompatVersionComparison( + CSS_NESTING_MIN_VERSION, + addonMetadata?.firefoxStrictMinVersion + ) + ) { + return []; + } + const messageList = []; if (cssNode.type === 'rule') { for (let i = 0; i < cssNode.nodes.length; i++) { diff --git a/src/scanners/base.js b/src/scanners/base.js index 4004c9b1bf..ef7121ac3f 100644 --- a/src/scanners/base.js +++ b/src/scanners/base.js @@ -27,6 +27,18 @@ export default class BaseScanner { throw new Error('scannerName is not implemented'); } + /** + * @typedef {Object} ScannerOptions + * @property {import('../parsers/manifestjson').Metadata} addonMetadata + * @property {import('../collector').default} collector + * @property {string[]} disabledRules + * @property {string[]} existingFiles + * @property {boolean} privileged + * + * @param {string|Buffer|import('stream').Readable} contents + * @param {string} filename + * @param {ScannerOptions} options + */ constructor(contents, filename, options = {}) { this.contents = contents; this.filename = filename; diff --git a/src/utils.js b/src/utils.js index 45c1d671b2..9795f7deb3 100644 --- a/src/utils.js +++ b/src/utils.js @@ -415,6 +415,11 @@ export function androidStrictMinVersion(manifestJson) { return version; } +/** + * @param {*} versionAdded + * @param {number} minVersion + * @returns {boolean} true if versionAdded has a strictly greater major version than minVersion + */ export function basicCompatVersionComparison(versionAdded, minVersion) { const asNumber = parseInt(versionAdded, 10); return !Number.isNaN(asNumber) && asNumber > minVersion; diff --git a/tests/unit/rules/css/test.invalidNesting.js b/tests/unit/rules/css/test.invalidNesting.js index cfbcf96781..a6c7dcf442 100644 --- a/tests/unit/rules/css/test.invalidNesting.js +++ b/tests/unit/rules/css/test.invalidNesting.js @@ -5,22 +5,48 @@ import { VALIDATION_WARNING } from 'const'; import CSSScanner from 'scanners/css'; describe('CSS Rule InvalidNesting', () => { - it('should detect invalid nesting', async () => { - const code = oneLine`/* I'm a comment */ + it.each([60, 116])( + 'should detect invalid nesting when firefoxStrictMinVersion=%s', + async (firefoxStrictMinVersion) => { + const code = oneLine`/* I'm a comment */ #something { .bar { height: 100px; } }`; - const cssScanner = new CSSScanner(code, 'fakeFile.css'); + const cssScanner = new CSSScanner(code, 'fakeFile.css', { + addonMetadata: { + firefoxStrictMinVersion, + }, + }); - const { linterMessages } = await cssScanner.scan(); - expect(linterMessages.length).toEqual(1); - expect(linterMessages[0].code).toEqual( - messages.INVALID_SELECTOR_NESTING.code - ); - expect(linterMessages[0].type).toEqual(VALIDATION_WARNING); - }); + const { linterMessages } = await cssScanner.scan(); + expect(linterMessages.length).toEqual(1); + expect(linterMessages[0].code).toEqual( + messages.INVALID_SELECTOR_NESTING.code + ); + expect(linterMessages[0].type).toEqual(VALIDATION_WARNING); + } + ); + + it.each([117, 118])( + 'should not report invalid nesting when firefoxStrictMinVersion=%s', + async (firefoxStrictMinVersion) => { + const code = oneLine`/* I'm a comment */ + #something { + .bar { + height: 100px; + } + }`; + const cssScanner = new CSSScanner(code, 'fakeFile.css', { + addonMetadata: { + firefoxStrictMinVersion, + }, + }); + const { linterMessages } = await cssScanner.scan(); + expect(linterMessages.length).toEqual(0); + } + ); it('should not detect invalid nesting', async () => { const code = oneLine`/* I'm a comment */ diff --git a/tests/unit/test.utils.js b/tests/unit/test.utils.js index 43b91aec51..17afae85db 100644 --- a/tests/unit/test.utils.js +++ b/tests/unit/test.utils.js @@ -602,6 +602,10 @@ describe('basicCompatVersionComparison', () => { expect(basicCompatVersionComparison('61', 60)).toBe(true); }); + it('should return false when version added is equal to min version', () => { + expect(basicCompatVersionComparison('61.5.2', 61)).toBe(false); + }); + it('should return false when version added is smaller than min version', () => { expect(basicCompatVersionComparison('59', 60)).toBe(false); });