tor-browser

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

commit b7da164f64ec9d92c357f51d28ef8f52906c195b
parent 453c9ef630ee2124ef3665f430fff0f524bd538f
Author: Osmond Arnesto <oarnesto@mozilla.com>
Date:   Tue,  4 Nov 2025 16:59:07 +0000

Bug 1988860 - stylelint rule to enforce using size design tokens r=frontend-codestyle-reviewers,akulyk,Gijs

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

Diffstat:
M.stylelintrc.js | 3+++
Adocs/code-quality/lint/linters/stylelint-plugin-mozilla/rules/use-size-tokens.rst | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mstylelint-rollouts.config.js | 307++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mtools/lint/stylelint/stylelint-plugin-mozilla/helpers.mjs | 95++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mtools/lint/stylelint/stylelint-plugin-mozilla/rules/index.mjs | 2++
Atools/lint/stylelint/stylelint-plugin-mozilla/rules/use-size-tokens.mjs | 172+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/lint/stylelint/stylelint-plugin-mozilla/tests/use-size-tokens.tests.mjs | 192+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 878 insertions(+), 4 deletions(-)

diff --git a/.stylelintrc.js b/.stylelintrc.js @@ -283,6 +283,7 @@ module.exports = { "stylelint-plugin-mozilla/use-text-color-tokens": true, "stylelint-plugin-mozilla/use-box-shadow-tokens": true, "stylelint-plugin-mozilla/no-non-semantic-token-usage": true, + "stylelint-plugin-mozilla/use-size-tokens": true, }, overrides: [ @@ -431,6 +432,7 @@ module.exports = { "stylelint-plugin-mozilla/use-text-color-tokens": null, "stylelint-plugin-mozilla/use-box-shadow-tokens": null, "stylelint-plugin-mozilla/no-non-semantic-token-usage": null, + "stylelint-plugin-mozilla/use-size-tokens": null, }, }, { @@ -446,6 +448,7 @@ module.exports = { "stylelint-plugin-mozilla/use-space-tokens": true, "stylelint-plugin-mozilla/use-text-color-tokens": true, "stylelint-plugin-mozilla/no-non-semantic-token-usage": true, + "stylelint-plugin-mozilla/use-size-tokens": true, }, }, { diff --git a/docs/code-quality/lint/linters/stylelint-plugin-mozilla/rules/use-size-tokens.rst b/docs/code-quality/lint/linters/stylelint-plugin-mozilla/rules/use-size-tokens.rst @@ -0,0 +1,111 @@ +=============== +use-size-tokens +=============== + +This rule checks that CSS declarations use size design token variables +instead of hardcoded values. + +Examples of incorrect token usage for this rule: +------------------------------------------------ + +.. code-block:: css + + .card { + min-width: 48px; + } + +.. code-block:: css + + .button { + height: 0.75rem; + } + +.. code-block:: css + + .icon { + width: 20px; + } + + +.. code-block:: css + + .icon { + width: calc(2 * 16px); + } + +.. code-block:: css + + :root { + --local-size: 24px; + } + + .button { + min-height: var(--local-size); + } + +Examples of correct code for this rule: +--------------------------------------- + +.. code-block:: css + + .card { + min-width: var(--size-item-large); + } + +.. code-block:: css + + .button { + height: var(--button-min-height); + } + +.. code-block:: css + + .icon { + width: var(--icon-size-medium); + } + +.. code-block:: css + + .icon { + width: var(--icon-size-medium, 28px); + } + +.. code-block:: css + + .icon { + width: calc(2 * var(--icon-size-medium)); + } + +.. code-block:: css + + .icon { + width: calc(2 * var(--icon-size-medium, 28px)); + } + +.. code-block:: css + + :root { + --local-size: var(--size-item-small); + } + + .button { + min-height: var(--local-size); + } + +.. code-block:: css + + .button { + width: 100%; + } + +.. code-block:: css + + .button { + width: auto; + } + +.. code-block:: css + + .icon { + max-height: 2em; + } diff --git a/stylelint-rollouts.config.js b/stylelint-rollouts.config.js @@ -1507,7 +1507,7 @@ module.exports = [ // stylelint fixes for this rule will be addressed in Bug 1992749 name: "rollout-no-non-semantic-token-usage", rules: { - "stylelint-plugin-mozilla/no-non-semantic-token-usage": "null", + "stylelint-plugin-mozilla/no-non-semantic-token-usage": null, }, files: [ "browser/components/aboutlogins/content/components/input-field/input-field.css", @@ -1587,4 +1587,309 @@ module.exports = [ "toolkit/themes/shared/menulist.css", ], }, + { + // stylelint fixes for this rule will be addressed in Bug 1995685 + name: "rollout-use-size-tokens", + rules: { + "stylelint-plugin-mozilla/use-size-tokens": null, + }, + files: [ + "browser/base/content/aboutDialog.css", + "browser/base/content/pageinfo/pageInfo.css", + "browser/branding/aurora/stubinstaller/installing_page.css", + "browser/branding/aurora/stubinstaller/profile_cleanup_page.css", + "browser/branding/nightly/stubinstaller/installing_page.css", + "browser/branding/nightly/stubinstaller/profile_cleanup_page.css", + "browser/branding/official/stubinstaller/installing_page.css", + "browser/branding/official/stubinstaller/profile_cleanup_page.css", + "browser/branding/unofficial/content/aboutDialog.css", + "browser/branding/unofficial/stubinstaller/installing_page.css", + "browser/branding/unofficial/stubinstaller/profile_cleanup_page.css", + "browser/components/aboutlogins/content/aboutLogins.css", + "browser/components/aboutlogins/content/aboutLoginsImportReport.css", + "browser/components/aboutlogins/content/components/confirmation-dialog.css", + "browser/components/aboutlogins/content/components/fxaccounts-button.css", + "browser/components/aboutlogins/content/components/generic-dialog.css", + "browser/components/aboutlogins/content/components/login-alert.css", + "browser/components/aboutlogins/content/components/login-filter.css", + "browser/components/aboutlogins/content/components/login-intro.css", + "browser/components/aboutlogins/content/components/login-item.css", + "browser/components/aboutlogins/content/components/login-list-lit-item.css", + "browser/components/aboutlogins/content/components/login-list.css", + "browser/components/aboutlogins/content/components/login-message-popup.css", + "browser/components/aboutlogins/content/components/login-timeline.css", + "browser/components/aboutlogins/content/components/menu-button.css", + "browser/components/aboutlogins/content/components/remove-logins-dialog.css", + "browser/components/aboutwelcome/content-src/aboutwelcome.scss", + "browser/components/asrouter/content-src/components/ASRouterAdmin/ASRouterAdmin.scss", + "browser/components/asrouter/content-src/styles/_feature-callout.scss", + "browser/components/backup/content/backup-settings.css", + "browser/components/backup/content/password-rules-tooltip.css", + "browser/components/backup/content/password-validation-inputs.css", + "browser/components/backup/content/restore-from-backup.css", + "browser/components/backup/content/turn-on-scheduled-backups.css", + "browser/components/contextualidentity/content/usercontext.css", + "browser/components/enterprisepolicies/content/aboutPolicies.css", + "browser/components/firefoxview/card-container.css", + "browser/components/firefoxview/firefoxview.css", + "browser/components/firefoxview/fxview-empty-state.css", + "browser/components/firefoxview/fxview-tab-row.css", + "browser/components/firefoxview/opentabs-tab-row.css", + "browser/components/firefoxview/view-syncedtabs.css", + "browser/components/genai/chat.css", + "browser/components/genai/content/link-preview-card.css", + "browser/components/genai/content/model-optin.css", + "browser/components/messagepreview/messagepreview.css", + "browser/components/places/content/clearDataForSite.css", + "browser/components/places/metadataViewer/interactionsViewer.css", + "browser/components/preferences/dialogs/sitePermissions.css", + "browser/components/preferences/widgets/placeholder-message/placeholder-message.css", + "browser/components/profiles/content/avatar.css", + "browser/components/profiles/content/edit-profile-card.css", + "browser/components/profiles/content/profile-avatar-selector.css", + "browser/components/profiles/content/profile-card.css", + "browser/components/profiles/content/profile-selector.css", + "browser/components/profiles/content/profiles-pages.css", + "browser/components/profiles/content/profiles-theme-card.css", + "browser/components/protections/content/protections.css", + "browser/components/screenshots/overlay/overlay.css", + "browser/components/screenshots/screenshots-buttons.css", + "browser/components/search/content/contentSearchUI.css", + "browser/components/search/test/browser/telemetry/serp.css", + "browser/components/sidebar/sidebar-main.css", + "browser/components/sidebar/sidebar-pins-promo.css", + "browser/components/sidebar/sidebar.css", + "browser/components/textrecognition/textrecognition.css", + "browser/components/urlbar/tests/browser/dynamicResult0.css", + "browser/components/urlbar/tests/browser/dynamicResult1.css", + "browser/components/webrtc/content/webrtc-preview/webrtc-preview.css", + "browser/extensions/formautofill/content/formautofill.css", + "browser/extensions/formautofill/content/manageDialog.css", + "browser/extensions/formautofill/skin/shared/editAddress.css", + "browser/extensions/formautofill/skin/shared/editDialog-shared.css", + "browser/extensions/newtab/content-src/components/Base/_Base.scss", + "browser/extensions/newtab/content-src/components/Card/_Card.scss", + "browser/extensions/newtab/content-src/components/CollapsibleSection/_CollapsibleSection.scss", + "browser/extensions/newtab/content-src/components/ConfirmDialog/_ConfirmDialog.scss", + "browser/extensions/newtab/content-src/components/ContextMenu/_ContextMenu.scss", + "browser/extensions/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/AdBanner/_AdBanner.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/AdBannerContextMenu/AdBannerContextMenu.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/_CardGrid.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/CardSections/_CardSections.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/_DSContextFooter.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSEmptyState/_DSEmptyState.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSImage/_DSImage.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSMessage/_DSMessage.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/_DownloadMobilePromoHighlight.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/_FeatureHighlight.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/_FollowSectionButtonHighlight.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/_ShortcutFeatureHighlight.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/_WallpaperFeatureHighlight.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/Highlights/_Highlights.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/Navigation/_Navigation.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/PersonalizedCard/_PersonalizedCard.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/PromoCard/_PromoCard.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/ReportContent/ReportContent.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/SectionContextMenu/_SectionContextMenu.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/TopicSelection/_TopicSelection.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/TopicsWidget/_TopicsWidget.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/TopSites/_TopSites.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/TrendingSearches/_TrendingSearches.scss", + "browser/extensions/newtab/content-src/components/DownloadModalToggle/_DownloadModalToggle.scss", + "browser/extensions/newtab/content-src/components/Logo/_Logo.scss", + "browser/extensions/newtab/content-src/components/ModalOverlay/_ModalOverlay.scss", + "browser/extensions/newtab/content-src/components/MoreRecommendations/_MoreRecommendations.scss", + "browser/extensions/newtab/content-src/components/Notifications/_Notifications.scss", + "browser/extensions/newtab/content-src/components/Search/_Search.scss", + "browser/extensions/newtab/content-src/components/Sections/_Sections.scss", + "browser/extensions/newtab/content-src/components/TopSites/_TopSites.scss", + "browser/extensions/newtab/content-src/components/WallpaperCategories/_WallpaperCategories.scss", + "browser/extensions/newtab/content-src/components/Weather/_Weather.scss", + "browser/extensions/newtab/content-src/components/Widgets/_Widgets.scss", + "browser/extensions/newtab/content-src/components/Widgets/FocusTimer/_FocusTimer.scss", + "browser/extensions/newtab/content-src/components/Widgets/Lists/_Lists.scss", + "browser/extensions/newtab/content-src/styles/_icons.scss", + "browser/extensions/newtab/content-src/styles/_mixins.scss", + "browser/extensions/newtab/content-src/styles/_variables.scss", + "browser/extensions/newtab/content-src/styles/activity-stream.scss", + "browser/themes/linux/browser.css", + "browser/themes/linux/places/organizer.css", + "browser/themes/osx/browser.css", + "browser/themes/osx/places/organizer.css", + "browser/themes/shared/aboutSessionRestore.css", + "browser/themes/shared/addon-notification.css", + "browser/themes/shared/addons/extension-controlled.css", + "browser/themes/shared/addons/unified-extensions.css", + "browser/themes/shared/autocomplete.css", + "browser/themes/shared/blockedSite.css", + "browser/themes/shared/browser-shared.css", + "browser/themes/shared/controlcenter/panel.css", + "browser/themes/shared/customizableui/customizeMode.css", + "browser/themes/shared/customizableui/panelUI-shared.css", + "browser/themes/shared/downloads/allDownloadsView.inc.css", + "browser/themes/shared/downloads/download-blockedStates.css", + "browser/themes/shared/downloads/downloads.inc.css", + "browser/themes/shared/downloads/indicator.css", + "browser/themes/shared/downloads/progressmeter.css", + "browser/themes/shared/formautofill-notification.css", + "browser/themes/shared/identity-block/identity-block.css", + "browser/themes/shared/identity-credential-notification.css", + "browser/themes/shared/migration/migration-wizard.css", + "browser/themes/shared/notification-icons.css", + "browser/themes/shared/pageInfo.css", + "browser/themes/shared/places/editBookmark.css", + "browser/themes/shared/places/editBookmarkPanel.css", + "browser/themes/shared/places/organizer-shared.css", + "browser/themes/shared/places/sidebar.css", + "browser/themes/shared/places/tree-icons.css", + "browser/themes/shared/preferences/applications.css", + "browser/themes/shared/preferences/containers-dialog.css", + "browser/themes/shared/preferences/containers.css", + "browser/themes/shared/preferences/fxaPairDevice.css", + "browser/themes/shared/preferences/preferences.css", + "browser/themes/shared/preferences/privacy.css", + "browser/themes/shared/preferences/search.css", + "browser/themes/shared/preferences/siteDataSettings.css", + "browser/themes/shared/preferences/translations.css", + "browser/themes/shared/privatebrowsing/aboutPrivateBrowsing.css", + "browser/themes/shared/search/searchbar.css", + "browser/themes/shared/sidebar.css", + "browser/themes/shared/syncedtabs/sidebar.css", + "browser/themes/shared/tab-list-tree.css", + "browser/themes/shared/tabbrowser/content-area.css", + "browser/themes/shared/tabbrowser/ctrlTab.css", + "browser/themes/shared/tabbrowser/fullscreen-and-pointerlock.css", + "browser/themes/shared/tabbrowser/tab-hover-preview.css", + "browser/themes/shared/tabbrowser/tabs.css", + "browser/themes/shared/toolbarbuttons.css", + "browser/themes/shared/translations/panel.css", + "browser/themes/shared/UITour.css", + "browser/themes/shared/urlbar-dynamic-results.css", + "browser/themes/shared/urlbar-searchbar.css", + "browser/themes/shared/urlbarView.css", + "browser/themes/shared/webRTC-indicator.css", + "browser/themes/windows/places/organizer.css", + "devtools/client/aboutdebugging/src/base.css", + "devtools/client/aboutdebugging/src/components/App.css", + "devtools/client/aboutdebugging/src/components/debugtarget/ExtensionDetail.css", + "devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAction.css", + "devtools/client/aboutdebugging/src/components/ProfilerDialog.css", + "devtools/client/aboutdebugging/src/components/shared/IconLabel.css", + "devtools/client/aboutdebugging/src/components/sidebar/Sidebar.css", + "devtools/client/aboutdebugging/src/components/sidebar/SidebarFixedItem.css", + "devtools/client/aboutdebugging/src/components/sidebar/SidebarItem.css", + "devtools/client/aboutdebugging/src/components/sidebar/SidebarRuntimeItem.css", + "dom/events/test/pointerevents/wpt/pointerevent_styles.css", + "dom/xml/test/old/books/classic.css", + "dom/xml/test/old/books/common.css", + "gfx/layers/apz/test/mochitest/helper_subframe_style.css", + "layout/generic/test/frame_selection_underline.css", + "layout/style/res/forms.css", + "layout/style/res/html.css", + "layout/style/res/scrollbars.css", + "security/manager/pki/resources/content/certManager.css", + "testing/marionette/harness/marionette_harness/tests/unit/assets/chrome/style.css", + "testing/mochitest/static/harness.css", + "testing/mozbase/mozlog/mozlog/formatters/html/style.css", + "testing/talos/talos/tests/scroll/reader.css", + "testing/talos/talos/tests/tart/addon/content/tab-min-width-1px.css", + "toolkit/components/aboutcheckerboard/content/aboutCheckerboard.css", + "toolkit/components/aboutconfig/content/aboutconfig.css", + "toolkit/components/aboutinference/content/aboutInference.css", + "toolkit/components/aboutinference/content/model-files-view.css", + "toolkit/components/aboutprocesses/content/aboutProcesses.css", + "toolkit/components/aboutthirdparty/content/aboutThirdParty.css", + "toolkit/components/certviewer/content/certviewer.css", + "toolkit/components/certviewer/content/components/error-section.css", + "toolkit/components/certviewer/content/components/info-group.css", + "toolkit/components/normandy/content/about-studies/about-studies.css", + "toolkit/components/printing/content/print.css", + "toolkit/components/printing/content/printPagination.css", + "toolkit/components/printing/content/printPreview.css", + "toolkit/components/printing/content/toggle-group.css", + "toolkit/components/reader/moz-slider.css", + "toolkit/components/resistfingerprinting/content/letterboxing.css", + "toolkit/components/satchel/megalist/content/components/login-line/login-line.css", + "toolkit/components/satchel/megalist/content/components/password-card/password-card.css", + "toolkit/components/satchel/megalist/content/megalist.css", + "toolkit/components/translations/content/about-translations.css", + "toolkit/content/aboutGlean.css", + "toolkit/content/aboutLogging/aboutLogging.css", + "toolkit/content/aboutTelemetry.css", + "toolkit/content/aboutwebrtc/aboutWebrtc.css", + "toolkit/content/tests/widgets/videomask.css", + "toolkit/content/widgets/infobar.css", + "toolkit/content/widgets/moz-button/moz-button.css", + "toolkit/content/widgets/moz-input-color/moz-input-color.css", + "toolkit/content/widgets/moz-input-folder/moz-input-folder.css", + "toolkit/content/widgets/moz-input-text/moz-input-text.css", + "toolkit/content/widgets/moz-message-bar/moz-message-bar.css", + "toolkit/content/widgets/moz-page-nav/moz-page-nav-button.css", + "toolkit/content/widgets/moz-page-nav/moz-page-nav.css", + "toolkit/content/widgets/moz-promo/moz-promo.css", + "toolkit/content/widgets/moz-reorderable-list/moz-reorderable-list.css", + "toolkit/content/widgets/moz-select/moz-select.css", + "toolkit/content/widgets/moz-toggle/moz-toggle.css", + "toolkit/content/widgets/panel-list/panel-item.css", + "toolkit/content/widgets/panel-list/panel-list.css", + "toolkit/content/widgets/moz-box-group/moz-box-group.css", + "toolkit/content/xul.css", + "toolkit/crashreporter/content/crashes.css", + "toolkit/mozapps/extensions/content/aboutaddons.css", + "toolkit/mozapps/extensions/content/components/mlmodel-card-header-additions.css", + "toolkit/mozapps/extensions/content/shortcuts.css", + "toolkit/themes/linux/global/autocomplete.css", + "toolkit/themes/linux/mozapps/update/updates.css", + "toolkit/themes/osx/global/autocomplete.css", + "toolkit/themes/osx/global/button.css", + "toolkit/themes/osx/global/global.css", + "toolkit/themes/osx/global/in-content/common.css", + "toolkit/themes/osx/global/wizard.css", + "toolkit/themes/osx/mozapps/handling/handling.css", + "toolkit/themes/osx/mozapps/update/updates.css", + "toolkit/themes/shared/aboutHttpsOnlyError.css", + "toolkit/themes/shared/aboutLicense.css", + "toolkit/themes/shared/aboutNetError.css", + "toolkit/themes/shared/aboutReader.css", + "toolkit/themes/shared/aboutServiceWorkers.css", + "toolkit/themes/shared/aboutSupport.css", + "toolkit/themes/shared/alert.css", + "toolkit/themes/shared/appPicker.css", + "toolkit/themes/shared/checkbox.css", + "toolkit/themes/shared/close-icon.css", + "toolkit/themes/shared/commonDialog.css", + "toolkit/themes/shared/datetimeinputpickers.css", + "toolkit/themes/shared/design-system/tokens-table.css", + "toolkit/themes/shared/dirListing/dirListing.css", + "toolkit/themes/shared/downloads/unknownContentType.css", + "toolkit/themes/shared/error-pages.css", + "toolkit/themes/shared/findbar.css", + "toolkit/themes/shared/global-shared.css", + "toolkit/themes/shared/in-content/common-shared.css", + "toolkit/themes/shared/in-content/info-pages.css", + "toolkit/themes/shared/menu.css", + "toolkit/themes/shared/menulist.css", + "toolkit/themes/shared/narrate.css", + "toolkit/themes/shared/offlineSupportPages.css", + "toolkit/themes/shared/pictureinpicture/player.css", + "toolkit/themes/shared/pictureinpicture/texttracks.css", + "toolkit/themes/shared/popup.css", + "toolkit/themes/shared/popupnotification.css", + "toolkit/themes/shared/profileDowngrade.css", + "toolkit/themes/shared/radio.css", + "toolkit/themes/shared/splitter.css", + "toolkit/themes/shared/toolbar.css", + "toolkit/themes/shared/toolbarbutton.css", + "toolkit/themes/shared/tree/tree.css", + "toolkit/themes/windows/global/autocomplete.css", + "toolkit/themes/windows/global/global.css", + "toolkit/themes/windows/global/wizard.css", + "toolkit/themes/windows/mozapps/handling/handling.css", + "toolkit/themes/windows/mozapps/update/updates.css", + "tools/tryselect/selectors/chooser/static/style.css", + ], + }, ]; diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/helpers.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/helpers.mjs @@ -47,6 +47,11 @@ export const ALLOW_LIST = [ ]; /** + * Regex capturing numeric values, em values, ch values, and percentage values + */ +export const FIXED_UNITS = /^\d*\.?\d*(em|ch|%)?$/; + +/** * Extends our base ALLOW_LIST with additional allowed values. * * @param {string[]} additionalAllows to be appended to our list @@ -166,6 +171,9 @@ export const isFunction = node => node.type === "function"; export const isVariableFunction = node => isFunction(node) && node.value === "var"; +// checks if a node is a `calc()` function +export const isCalcFunction = node => isFunction(node) && node.value === "calc"; + // checks if a node is a url() function export const isUrlFunction = node => isFunction(node) && node.value === "url"; @@ -226,6 +234,19 @@ export const containsColorFunction = value => { }; /** + * Checks if a node contains a value using vw/vh units + * e.g., `100vh`. + * + * @param {string} value some CSS declaration to match + * @returns {boolean} + */ +export const containsViewportUnit = value => { + return valueParser(String(value)).nodes.some( + node => node.type === "word" && /^(0|[\d.]+)(vh|vw)$/.test(node.value) + ); +}; + +/** * Returns only the properties in the declaration that are colors, or at least likely to be colors. * This allows for ignoring properties in shorthand that are not relevant to color rules. * @@ -262,15 +283,20 @@ export const getColorProperties = value => { export const isToken = (value, tokenCSS) => tokenCSS.includes(value); /** - * Checks if a CSS value is allowed. + * Checks if a CSS value is allowed, given exact strings or a + * regex pattern in an allowList. * * @param {string} value some CSS declaration to match * @param {string[]} allowList * @returns {boolean} */ export const isAllowed = (value, allowList) => { + const allowListPattern = pattern => { + pattern instanceof RegExp ? pattern.test(value) : pattern === value; + }; + // If the value is in the allowList - if (allowList.includes(value)) { + if (allowList.some(allowListPattern)) { return true; } @@ -281,7 +307,13 @@ export const isAllowed = (value, allowList) => { // If the value is in the allowList but the string is CSS shorthand, e.g. `border` properties return valueParser(value).nodes.some( - node => isWord(node) && allowList.includes(node.value) + node => + isWord(node) && + allowList.some(pattern => + pattern instanceof RegExp + ? pattern.test(node.value) + : pattern === node.value + ) ); }; @@ -432,6 +464,63 @@ export const isValidTokenUsage = ( }; /** + * Checks if a calc() function contains valid token usage. + * + * @param {string} value - CSS declaration to match + * @param {string[]} tokenCSS + * @param {object} cssCustomProperties + * @param {string[]} allowList + * @returns {boolean} + */ +export const isValidTokenUsageInCalc = ( + value, + tokenCSS, + cssCustomProperties, + allowList = ALLOW_LIST +) => { + const parsed = valueParser(String(value)); + let isEveryChildValid = true; + + parsed.walk(node => { + if (!isEveryChildValid || !isCalcFunction(node)) { + return; + } + + isEveryChildValid = node.nodes.every(child => { + if ( + child.type === "space" || + (child.type === "word" && /^[+\-*/]$/.test(child.value)) + ) { + return true; + } + + if (child.type === "word") { + if ( + isAllowed(child.value, allowList) || + /^\d*\.?\d*(vh|vw|em|%)?$/.test(child.value) + ) { + return true; + } + } + + if (isVariableFunction(child)) { + const variableNode = `var(${child.nodes[0].value})`; + if ( + isToken(variableNode, tokenCSS) || + isValidLocalProperty(variableNode, cssCustomProperties, tokenCSS) + ) { + return true; + } + } + + return false; + }); + }); + + return isEveryChildValid; +}; + +/** * Checks if CSS value uses color tokens correctly. * * @param {string} value some CSS declaration to match diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/rules/index.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/rules/index.mjs @@ -15,6 +15,7 @@ import useBackgroundColorTokens from "./use-background-color-tokens.mjs"; import useTextColorTokens from "./use-text-color-tokens.mjs"; import useBoxShadowTokens from "./use-box-shadow-tokens.mjs"; import noNonSemanticTokenUsage from "./no-non-semantic-token-usage.mjs"; +import useSizeTokens from "./use-size-tokens.mjs"; export default { "no-base-design-tokens": noBaseDesignTokens, @@ -28,4 +29,5 @@ export default { "use-text-color-tokens": useTextColorTokens, "use-box-shadow-tokens": useBoxShadowTokens, "no-non-semantic-token-usage": noNonSemanticTokenUsage, + "use-size-tokens": useSizeTokens, }; diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/rules/use-size-tokens.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/rules/use-size-tokens.mjs @@ -0,0 +1,172 @@ +/* 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/. */ + +import stylelint from "stylelint"; +import valueParser from "postcss-value-parser"; +import { + namespace, + createTokenNamesArray, + createRawValuesObject, + isValidTokenUsage, + isValidTokenUsageInCalc, + containsViewportUnit, + getLocalCustomProperties, + usesRawFallbackValues, + usesRawShorthandValues, + createAllowList, + FIXED_UNITS, +} from "../helpers.mjs"; + +const { + utils: { report, ruleMessages, validateOptions }, +} = stylelint; + +const ruleName = namespace("use-size-tokens"); + +const messages = ruleMessages(ruleName, { + rejected: value => + `Consider using a size design token instead of ${value}. This may be fixable by running the same command again with --fix.`, +}); + +const meta = { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/stylelint-plugin-mozilla/rules/use-size-tokens.html", + fixable: true, +}; + +// Bug 1979978 moves this object to ../data.mjs +const SIZE = { + CATEGORIES: ["size", "icon-size"], + PROPERTIES: [ + "width", + "min-width", + "max-width", + "height", + "min-height", + "max-height", + "inline-size", + "min-inline-size", + "max-inline-size", + "block-size", + "min-block-size", + "max-block-size", + "inset", + "inset-block", + "inset-block-end", + "inset-block-start", + "inset-inline", + "inset-inline-end", + "inset-inline-start", + "left", + "right", + "top", + "bottom", + "background-size", + ], +}; + +const tokenCSS = createTokenNamesArray(SIZE.CATEGORIES); + +// Allowed size values in CSS +const SIZE_TOKENS_ALLOW_LIST = createAllowList([ + FIXED_UNITS, + "0", + "auto", + "none", + "fit-content", + "min-content", + "max-content", +]); + +const RAW_VALUE_TO_TOKEN_VALUE = { + ...createRawValuesObject(SIZE.CATEGORIES), + "0.75rem": "var(--size-item-xsmall)", + "1rem": "var(--size-item-small)", + "1.5rem": "var(--size-item-medium)", + "2rem": "var(--size-item-large)", + "3rem": "var(--size-item-xlarge)", +}; + +const ruleFunction = primaryOption => { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: primaryOption, + possible: [true], + }); + + if (!validOptions) { + return; + } + + // Walk declarations once to generate a lookup table of variables. + const cssCustomProperties = getLocalCustomProperties(root); + + // Walk declarations again to detect non-token values. + root.walkDecls(declarations => { + if (!SIZE.PROPERTIES.includes(declarations.prop)) { + return; + } + + // Allows values using `vh` or `vw` units + if (containsViewportUnit(declarations.value)) { + return; + } + + // Otherwise, see if we are using the tokens correctly + if ( + isValidTokenUsage( + declarations.value, + tokenCSS, + cssCustomProperties, + SIZE_TOKENS_ALLOW_LIST + ) && + isValidTokenUsageInCalc( + declarations.value, + tokenCSS, + cssCustomProperties, + SIZE_TOKENS_ALLOW_LIST + ) && + !usesRawFallbackValues(declarations.value, RAW_VALUE_TO_TOKEN_VALUE) && + !usesRawShorthandValues( + declarations.value, + tokenCSS, + cssCustomProperties, + SIZE_TOKENS_ALLOW_LIST + ) + ) { + return; + } + + report({ + message: messages.rejected(declarations.value), + node: declarations, + result, + ruleName, + fix: () => { + const val = valueParser(declarations.value); + let hasFixes = false; + + val.walk(node => { + if (node.type === "word") { + const token = RAW_VALUE_TO_TOKEN_VALUE[node.value.trim()]; + if (token) { + hasFixes = true; + node.value = token; + } + } + }); + + if (hasFixes) { + declarations.value = val.toString(); + } + }, + }); + }); + }; +}; + +ruleFunction.ruleName = ruleName; +ruleFunction.messages = messages; +ruleFunction.meta = meta; + +export default ruleFunction; diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/tests/use-size-tokens.tests.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/tests/use-size-tokens.tests.mjs @@ -0,0 +1,192 @@ +/** + * 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-disable-next-line import/no-unresolved +import { testRule } from "stylelint-test-rule-node"; +import stylelint from "stylelint"; +import useSizeTokens from "../rules/use-size-tokens.mjs"; + +let plugin = stylelint.createPlugin(useSizeTokens.ruleName, useSizeTokens); +let { + ruleName, + rule: { messages }, +} = plugin; + +testRule({ + plugins: [plugin], + ruleName, + config: true, + fix: true, + accept: [ + { + code: ".a { max-height: var(--size-item-medium); }", + description: + "Using the medium item size token with the max-height property is valid.", + }, + { + code: ".a { min-height: var(--size-item-large); }", + description: + "Using the large item size token with the min-height property is valid.", + }, + { + code: ".a { max-width: var(--size-item-xlarge); }", + description: + "Using the xlarge item size token with the max-width property is valid.", + }, + { + code: ".a { min-width: var(--size-item-medium); }", + description: + "Using the medium item size token with the min-width property is valid.", + }, + { + code: ".a { min-width: var(--size-item-medium); }", + description: + "Using the medium item size token with the min-width property is valid.", + }, + { + code: ".a { inline-size: 50%; }", + description: + "Using a percentage value with the inline-size property is valid.", + }, + { + code: ".a { inline-size: var(--size-item-xlarge); }", + description: + "Using the xlarge item size token with the inline-size property is valid.", + }, + { + code: ` + @media (min-width: 768px) { + a. { color: var(--text-color); } + } + `, + description: + "Using a pixel value with a size property within an atrule is valid.", + }, + { + code: ".a { width: calc(var(--size-item-small) * 2); }", + description: + "Using the small item size token in a `calc()` declaration with the width property is valid.", + }, + { + code: ".a { width: calc(2 * var(--size-item-small)); }", + description: + "Using the small item size token in a `calc()` declaration with the width property is valid.", + }, + { + code: ".a { width: calc(2 * var(--size-item-small, 14px)); }", + description: + "Using the small item size token with a fallback value in a `calc()` declaration with the width property is valid.", + }, + { + code: ".a { width: calc(var(--size-item-small, 14px) * 2); }", + description: + "Using the small item size token with a fallback value in a `calc()` declaration with the width property is valid.", + }, + { + code: ".a { height: 100vh; }", + description: + "Using a value using the `vh` unit in the height property is valid.", + }, + { + code: ".a { height: calc(100vh - 2em); }", + description: + "Using a value using the `vh` unit and a percentage value in a `calc()` function in the height property is valid.", + }, + { + code: ".a { height: calc(100% - 2em); }", + description: + "Using a percentage value and a value using the `em` unit in a `calc()` function in the height property is valid.", + }, + { + code: ".a { width: calc(100vw - 10%); }", + description: + "Using a percentage value and a value using the `em` unit in a `calc()` function in the height property is valid.", + }, + { + code: ".a { min-block-size: 8em; }", + description: + "Using a value using the `em` unit in the min-block-size property is valid.", + }, + { + code: ".a { max-block-size: 75%; }", + description: + "Using a percentage value in the min-block-size property is valid.", + }, + { + code: ".a { max-height: var(--size-item-medium, 50%); }", + description: + "Using a size design token with a fallback value in the max-height property is valid.", + }, + { + code: ".a { min-block-size: fit-content; }", + description: + "Using the fit-content value in the min-block-size property is valid.", + }, + { + code: ".a { background-size: var(--icon-size-medium) auto; }", + description: + "Using the medium icon size token and the auto value in the background-size property is valid.", + }, + { + code: ".a { background-size: calc(100vh * 0.8); }", + description: + "Using the medium icon size token and the auto value in the background-size property is valid.", + }, + ], + reject: [ + { + code: ".a { max-height: 500px; }", + unfixable: true, + message: messages.rejected("500px"), + description: + "Consider using a size design token instead of using a pixel value. This may be fixable by running the same command again with --fix.", + }, + { + code: ".a { height: 0.75rem; }", + fixed: ".a { height: var(--size-item-xsmall); }", + message: messages.rejected("0.75rem"), + description: + "Consider using a size design token instead of using a rem value. This may be fixable by running the same command again with --fix.", + }, + { + code: ` + :root { --local-size: 24px; } + .a { min-height: var(--local-size); } + `, + unfixable: true, + message: messages.rejected("var(--local-size)"), + description: + "Consider using a size design token instead of using a pixel value. This may be fixable by running the same command again with --fix.", + }, + { + code: `.a { max-inline-size: calc(16px + 32px); }`, + fixed: `.a { max-inline-size: calc(var(--size-item-small) + var(--size-item-large)); }`, + message: messages.rejected("calc(16px + 32px)"), + description: + "Consider using a size design token instead of using a pixel value. This may be fixable by running the same command again with --fix.", + }, + { + code: `.a { max-inline-size: calc(16px + var(--size-item-xlarge)); }`, + fixed: `.a { max-inline-size: calc(var(--size-item-small) + var(--size-item-xlarge)); }`, + message: messages.rejected("calc(16px + var(--size-item-xlarge))"), + description: + "Consider using a size design token instead of using a pixel value. This may be fixable by running the same command again with --fix.", + }, + { + code: `.a { max-inline-size: calc(var(--size-item-small) + 32px); }`, + fixed: `.a { max-inline-size: calc(var(--size-item-small) + var(--size-item-large)); }`, + message: messages.rejected("calc(var(--size-item-small) + 32px)"), + description: + "Consider using a size design token instead of using a pixel value. This may be fixable by running the same command again with --fix.", + }, + { + code: `.a { max-block-size: calc(100vh + 32px); }`, + fixed: `.a { max-block-size: calc(100vh + var(--size-item-large)); }`, + message: messages.rejected("calc(100vh + 32px)"), + description: + "Consider using a size design token instead of using a pixel value. This may be fixable by running the same command again with --fix.", + }, + ], +});