tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit f87d25b06d368ccddf116e06e4d7a53e703fa23f
parent 2aee4803764a306053a363386eb40fcc9b187f4e
Author: Jon Oliver <jooliver@mozilla.com>
Date:   Mon, 20 Oct 2025 16:41:55 +0000

Bug 1953499: Add stylelint rule to prevent importing browser CSS files in toolkit r=frontend-codestyle-reviewers,mkennedy,Gijs

- Add no-browser-refs-in-toolkit stylelint rule
- Configure rule to only apply to toolkit CSS files
- Add tests for no-browser-refs-in-toolkit rule
- Add docs for no-browser-refs-in-toolkit rule

Differential Revision: https://phabricator.services.mozilla.com/D268275

Diffstat:
M.stylelintrc.js | 6++++++
Adocs/code-quality/lint/linters/stylelint-plugin-mozilla/rules/no-browser-refs-in-toolkit.rst | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtools/lint/stylelint/stylelint-plugin-mozilla/helpers.mjs | 3+++
Mtools/lint/stylelint/stylelint-plugin-mozilla/rules/index.mjs | 2++
Atools/lint/stylelint/stylelint-plugin-mozilla/rules/no-browser-refs-in-toolkit.mjs | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/lint/stylelint/stylelint-plugin-mozilla/tests/no-browser-refs-in-toolkit.tests.mjs | 145+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 379 insertions(+), 0 deletions(-)

diff --git a/.stylelintrc.js b/.stylelintrc.js @@ -442,5 +442,11 @@ module.exports = { "stylelint-plugin-mozilla/use-text-color-tokens": false, }, }, + { + files: ["toolkit/**/*.css", "toolkit/**/*.scss"], + rules: { + "stylelint-plugin-mozilla/no-browser-refs-in-toolkit": true, + }, + }, ], }; diff --git a/docs/code-quality/lint/linters/stylelint-plugin-mozilla/rules/no-browser-refs-in-toolkit.rst b/docs/code-quality/lint/linters/stylelint-plugin-mozilla/rules/no-browser-refs-in-toolkit.rst @@ -0,0 +1,120 @@ +========================== +no-browser-refs-in-toolkit +========================== + +This rule prevents toolkit code from importing browser-specific CSS or SVG files. +It is applied to both ``@import`` statements and CSS property values using the ``url`` function. + +This ensures that toolkit code remains independent and can be used in other applications/contexts +where browser code is not necessarily available. Browser code can depend on toolkit code, but toolkit +code should never depend on browser code, since toolkit code can be used in +other applications/contexts where browser code may not be available. + +See additional documentation for more information on `internal URLs </toolkit/internal-urls.html>`_. + +Rule Scope +---------- + +This rule only applies to CSS files within the ``toolkit/`` directory. It is +configured as an override in the stylelint configuration to target only +``toolkit/**/*.css`` files. + +Examples of incorrect usage for this rule: +------------------------------------------ + +Incorrect usage of ``@import`` statements: + +.. code-block:: css + + @import "chrome://browser/skin/example-file.css"; + +.. code-block:: css + + @import url("chrome://browser/skin/example-file.css"); + +.. code-block:: css + + @import "resource:///modules/example-file.css"; + +.. code-block:: css + + @import url("resource:///modules/example-file.css"); + +.. code-block:: css + + @import "resource://app/modules/example-file.css"; + +.. code-block:: css + + @import url("resource://app/modules/example-file.css"); + +.. code-block:: css + + @import "moz-src:///browser/example-file.css"; + +.. code-block:: css + + @import url("moz-src:///browser/example-file.css"); + +Incorrect usage of property values: + +.. code-block:: css + + .background-image { + background-image: url("chrome://browser/skin/example-file.svg"); + } + +.. code-block:: css + + .background-image { + background: url("moz-src:///browser/example-file.svg"); + } + +.. code-block:: css + + .background-image { + background: url("moz-src://foo/browser/example-file.svg"); + } + +Examples of correct usage for this rule: +---------------------------------------- + +Correct usage of ``@import`` statements: + +.. code-block:: css + + @import "chrome://global/skin/example-file.css"; + +.. code-block:: css + + @import url("chrome://global/skin/example-file.css"); + +.. code-block:: css + + @import "example-file.css"; + +.. code-block:: css + + @import url("example-file.css"); + +.. code-block:: css + + @import "resource://content-accessible/example-file.css" + +.. code-block:: css + + @import url("resource://content-accessible/example-file.css"); + +Correct usage of property values: + +.. code-block:: css + + .background-image { + background: url("chrome://global/skin/example-file.svg"); + } + +.. code-block:: css + + .background-image { + background: url("example-file.svg"); + } diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/helpers.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/helpers.mjs @@ -167,6 +167,9 @@ export const isFunction = node => node.type === "function"; export const isVariableFunction = node => isFunction(node) && node.value === "var"; +// checks if a node is a url() function +export const isUrlFunction = node => isFunction(node) && node.value === "url"; + /** * Checks if CSS includes a named color, e.g. 'white' or 'rebeccapurple' * diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/rules/index.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/rules/index.mjs @@ -5,6 +5,7 @@ */ import noBaseDesignTokens from "./no-base-design-tokens.mjs"; +import noBrowserRefsInToolkit from "./no-browser-refs-in-toolkit.mjs"; import useBorderRadiusTokens from "./use-border-radius-tokens.mjs"; import useBorderColorTokens from "./use-border-color-tokens.mjs"; import useFontSizeTokens from "./use-font-size-tokens.mjs"; @@ -16,6 +17,7 @@ import useBoxShadowTokens from "./use-box-shadow-tokens.mjs"; export default { "no-base-design-tokens": noBaseDesignTokens, + "no-browser-refs-in-toolkit": noBrowserRefsInToolkit, "use-border-radius-tokens": useBorderRadiusTokens, "use-border-color-tokens": useBorderColorTokens, "use-font-size-tokens": useFontSizeTokens, diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/rules/no-browser-refs-in-toolkit.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/rules/no-browser-refs-in-toolkit.mjs @@ -0,0 +1,103 @@ +/* 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/. */ +/* eslint-env node */ + +import stylelint from "stylelint"; +import valueParser from "postcss-value-parser"; +import { namespace, isUrlFunction, isWord } from "../helpers.mjs"; + +const { + utils: { report, ruleMessages, validateOptions }, +} = stylelint; + +const ruleName = namespace("no-browser-refs-in-toolkit"); + +const messages = ruleMessages(ruleName, { + rejected: url => + `${url} is part of Desktop Firefox and cannot be used by this code (which has to also work elsewhere).`, +}); + +const meta = { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/stylelint-plugin-mozilla/rules/no-browser-refs-in-toolkit.html", + fixable: false, +}; + +function extractUrlFromNode(node) { + if (!node) { + return null; + } + + if (node.type === "string") { + return node.value; + } + + if (isUrlFunction(node)) { + const urlContent = node.nodes[0]; + if (urlContent && (urlContent.type === "string" || isWord(urlContent))) { + return urlContent.value; + } + } + + return null; +} + +function isBrowserUrl(url) { + return ( + url.startsWith("chrome://browser") || + url.startsWith("resource:///") || + url.startsWith("resource://app/") || + /moz-src:\/\/\w*\/browser\//.test(url) + ); +} + +const ruleFunction = primaryOption => { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: primaryOption, + possible: [true], + }); + + if (!validOptions) { + return; + } + + root.walkAtRules("import", atRule => { + const params = valueParser(atRule.params); + const firstParam = params.nodes[0]; + const importUrl = extractUrlFromNode(firstParam); + + if (importUrl && isBrowserUrl(importUrl)) { + report({ + message: messages.rejected(importUrl), + node: atRule, + result, + ruleName, + }); + } + }); + + root.walkDecls(decl => { + const parsed = valueParser(decl.value); + + parsed.nodes.forEach(node => { + const url = extractUrlFromNode(node); + + if (url && isBrowserUrl(url)) { + report({ + message: messages.rejected(url), + node: decl, + result, + ruleName, + }); + } + }); + }); + }; +}; + +ruleFunction.ruleName = ruleName; +ruleFunction.messages = messages; +ruleFunction.meta = meta; + +export default ruleFunction; diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/tests/no-browser-refs-in-toolkit.tests.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/tests/no-browser-refs-in-toolkit.tests.mjs @@ -0,0 +1,145 @@ +/** + * 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/PL/2.0/. + */ + +// Bug 1948378: remove this exception when the eslint import plugin fully +// supports exports in package.json files +// eslint-disable-next-line import/no-unresolved +import { testRule } from "stylelint-test-rule-node"; +import stylelint from "stylelint"; +import noBrowserRefsInToolkit from "../rules/no-browser-refs-in-toolkit.mjs"; + +let plugin = stylelint.createPlugin( + noBrowserRefsInToolkit.ruleName, + noBrowserRefsInToolkit +); +let { + ruleName, + rule: { messages }, +} = plugin; + +testRule({ + plugins: [plugin], + ruleName, + config: [true], + fix: false, + accept: [ + // @import statements + { + code: '@import "chrome://global/skin/global.css";', + description: "Using chrome://global is valid.", + }, + { + code: '@import url("chrome://global/skin/global.css");', + description: "Using chrome://global with url function is valid.", + }, + { + code: '@import "close-icon.css";', + description: "Using relative path is valid.", + }, + { + code: '@import url("close-icon.css");', + description: "Using relative path with url function is valid.", + }, + { + code: '@import "resource://content-accessible/viewsource.css";', + description: "Using non-app resource is valid.", + }, + { + code: '@import url("resource://content-accessible/viewsource.css");', + description: "Using non-app resource with url function is valid.", + }, + // property values + { + code: 'a { background: url("chrome://global/skin/global.css"); }', + description: "Using chrome://global in property value is valid.", + }, + { + code: 'a { background: url("close-icon.svg"); }', + description: "Using relative path in property value is valid.", + }, + ], + + reject: [ + // @import statements + { + code: '@import "chrome://browser/skin/browser-colors.css";', + message: messages.rejected("chrome://browser/skin/browser-colors.css"), + description: "Using chrome://browser import should be flagged.", + }, + { + code: '@import url("chrome://browser/skin/browser-colors.css");', + message: messages.rejected("chrome://browser/skin/browser-colors.css"), + description: + "Using chrome://browser import with url function should be flagged.", + }, + { + code: '@import "resource:///modules/testfile.css";', + message: messages.rejected("resource:///modules/testfile.css"), + description: "Using resource:/// import should be flagged.", + }, + { + code: '@import url("resource:///modules/testfile.css");', + message: messages.rejected("resource:///modules/testfile.css"), + description: + "Using resource:/// import with url function should be flagged.", + }, + { + code: '@import "resource://app/modules/testfile.css";', + message: messages.rejected("resource://app/modules/testfile.css"), + description: "Using resource://app/ import should be flagged.", + }, + { + code: '@import url("resource://app/modules/testfile.css");', + message: messages.rejected("resource://app/modules/testfile.css"), + description: + "Using resource://app/ import with url function should be flagged.", + }, + { + code: '@import "moz-src:///browser/testfile.css";', + message: messages.rejected("moz-src:///browser/testfile.css"), + description: "Using moz-src browser path should be flagged.", + }, + { + code: '@import url("moz-src:///browser/testfile.css");', + message: messages.rejected("moz-src:///browser/testfile.css"), + description: + "Using moz-src browser path with url function should be flagged.", + }, + { + code: "background: url(chrome://browser/skin/notification-fill-12.svg) no-repeat center;", + message: messages.rejected( + "chrome://browser/skin/notification-fill-12.svg" + ), + description: + "Using chrome://browser in property value should be flagged.", + }, + // property values + { + code: 'a { background: url("chrome://browser/skin/browser-colors.css"); }', + message: messages.rejected("chrome://browser/skin/browser-colors.css"), + description: + "Using chrome://browser with quotes in property value should be flagged.", + }, + { + code: "a { background: url(moz-src:///browser/testfile.svg); }", + message: messages.rejected("moz-src:///browser/testfile.svg"), + description: + "Using moz-src browser path in property value should be flagged.", + }, + { + code: 'a { background: url("moz-src:///browser/testfile.svg"); }', + message: messages.rejected("moz-src:///browser/testfile.svg"), + description: + "Using moz-src browser path with quotes in property value should be flagged.", + }, + { + code: 'background-image: url("moz-src://foo/browser/testfile.svg");', + message: messages.rejected("moz-src://foo/browser/testfile.svg"), + description: + "Using moz-src browser path in property value should be flagged.", + }, + ], +});