diff --git a/.changeset/thick-lions-call.md b/.changeset/thick-lions-call.md new file mode 100644 index 00000000..826c3d13 --- /dev/null +++ b/.changeset/thick-lions-call.md @@ -0,0 +1,12 @@ +--- +"@primer/stylelint-config": major +--- + +**BREAKING CHANGE:** Removing plugins from the config. + +* primer/new-color-vars-have-fallback +* primer/no-deprecated-colors +* primer/no-override +* primer/no-scale-colors +* primer/no-undefined-vars +* primer/no-unused-vars diff --git a/README.md b/README.md index 872935b6..4ddd59e0 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,6 @@ Primer Stylelint Config extends the [stylelint-config-standard](https://github.c - [stylelint-order](https://github.com/hudochenkov/stylelint-order): Order-related linting rules for stylelint. Properties must be [sorted according to this list](https://github.com/primer/stylelint-config/blob/main/property-order.js). - [stylelint-scss](https://github.com/kristerkari/stylelint-scss): A collection of SCSS specific linting rules for stylelint - [scss/selector-no-redundant-nesting-selector](https://github.com/kristerkari/stylelint-scss/blob/master/src/rules/selector-no-redundant-nesting-selector/README.md): Disallow redundant nesting selectors (`&`). -- [primer/no-override](./plugins/#primerno-override): Prohibits custom styles that target Primer CSS selectors. -- [primer/no-unused-vars](./plugins/#primerno-unused-vars): Warns about SCSS variables that are declared by not used in your local files. -- [primer/no-undefined-vars](./plugins/#primerno-undefined-vars): Prohibits usage of undefined CSS variables. -- [primer/no-scale-colors](./plugins/#primerno-scale-colors): Prohibits the use of [non-functional scale CSS variables](https://primer.style/css/support/color-system#color-palette) - [primer/colors](./plugins/#primercolors): Enforces the use of certain color variables. - [primer/spacing](./plugins/#primerspacing): Enforces the use of spacing variables for margin and padding. - [primer/typography](./plugins/#primertypography): Enforces the use of typography variables for certain CSS properties. diff --git a/__tests__/__fixtures__/good/example.module.css b/__tests__/__fixtures__/good/example.module.css index 5721ba48..6349ec9f 100644 --- a/__tests__/__fixtures__/good/example.module.css +++ b/__tests__/__fixtures__/good/example.module.css @@ -3,9 +3,11 @@ overflow: hidden; } -.gradient { +:root { --offset: -500px; +} +.gradient { position: absolute; top: 0; /* stylelint-disable-next-line primer/responsive-widths */ @@ -18,7 +20,6 @@ } .gradient-left { - /* stylelint-disable primer/no-undefined-vars */ top: var(--offset); left: var(--offset); /* stylelint-disable-next-line primer/colors */ @@ -79,20 +80,20 @@ } .marketplace-item { - box-shadow: var(--shadow-resting-small, var(--color-shadow-small)); + box-shadow: var(--shadow-resting-small); } .marketplace-item:hover, .marketplace-item:focus-within { - background-color: var(--bgColor-muted, var(--color-canvas-subtle)); + background-color: var(--bgColor-muted); } .marketplace-item:focus-within { - outline: 2px solid var(--fgColor-accent, var(--color-accent-fg)); + outline: 2px solid var(--fgColor-accent); } .marketplace-item-link { - color: var(--fgColor-default, var(--color-fg-default)); + color: var(--fgColor-default); } .marketplace-item-link:hover { diff --git a/__tests__/new-color-vars-have-fallback.js b/__tests__/new-color-vars-have-fallback.js deleted file mode 100644 index 3bc3e1f6..00000000 --- a/__tests__/new-color-vars-have-fallback.js +++ /dev/null @@ -1,24 +0,0 @@ -import {ruleName} from '../plugins/new-color-vars-have-fallback.js' - -// eslint-disable-next-line no-undef -testRule({ - plugins: ['./plugins/new-color-vars-have-fallback'], - customSyntax: 'postcss-scss', - ruleName, - config: [true], - accept: [ - { - code: '.x { color: var(--fgColor-default, var(--color-fg-default)); }', - description: 'Variable has fallback', - }, - ], - reject: [ - { - code: '.x { color: var(--fgColor-default); }', - message: - 'Expected a fallback value for CSS variable --fgColor-default. New color variables fallbacks, check primer.style/primitives to find the correct value (primer/new-color-vars-have-fallback)', - line: 1, - column: 6, - }, - ], -}) diff --git a/__tests__/no-deprecated-colors.js b/__tests__/no-deprecated-colors.js deleted file mode 100644 index 4047d969..00000000 --- a/__tests__/no-deprecated-colors.js +++ /dev/null @@ -1,180 +0,0 @@ -import {ruleName} from '../plugins/no-deprecated-colors.js' - -// eslint-disable-next-line no-undef -testRule({ - plugins: ['./plugins/no-deprecated-colors'], - ruleName, - config: [ - true, - ], - fix: true, - accept: [ - {code: '.x { color: var(--fgColor-default); }'}, - { - code: '@include focusOutline(2px, var(--focus-outlineColor));', - }, - ], - reject: [ - { - code: '.x { color: var(--color-fg-default); }', - fixed: '.x { color: var(--fgColor-default); }', - message: `Variable --color-fg-default is deprecated for property color. Please use the replacement --fgColor-default. (primer/no-deprecated-colors)`, - line: 1, - column: 6, - }, - { - code: '.x { border-right: $border-width $border-style var(--color-border-muted); }', - fixed: '.x { border-right: $border-width $border-style var(--borderColor-muted); }', - message: `Variable --color-border-muted is deprecated for property border-right. Please use the replacement --borderColor-muted. (primer/no-deprecated-colors)`, - line: 1, - column: 6, - }, - { - code: '.x { border-color: var(--color-primer-border-contrast); }', - fixed: '.x { border-color: var(--borderColor-muted); }', - message: `Variable --color-primer-border-contrast is deprecated for property border-color. Please use the replacement --borderColor-muted. (primer/no-deprecated-colors)`, - line: 1, - column: 6, - }, - { - code: '.x { box-shadow: var(--color-fg-default); }', - unfixable: true, - message: `Variable --color-fg-default is deprecated for property box-shadow. Please consult the primer color docs for a replacement. https://primer.style/primitives/storybook/?path=/story/migration-tables (primer/no-deprecated-colors)`, - line: 1, - column: 6, - }, - { - code: '.x { background-color: var(--color-canvas-default-transparent); }', - fixed: '.x { background-color: var(--bgColor-transparent); }', - message: `Variable --color-canvas-default-transparent is deprecated for property background-color. Please use the replacement --bgColor-transparent. (primer/no-deprecated-colors)`, - line: 1, - column: 6, - }, - { - code: '.x { border: var(--borderWidth-thin) solid var(--color-border-default); }', - fixed: '.x { border: var(--borderWidth-thin) solid var(--borderColor-default); }', - message: `Variable --color-border-default is deprecated for property border. Please use the replacement --borderColor-default. (primer/no-deprecated-colors)`, - line: 1, - column: 6, - }, - { - code: '.x { border-color: var(--color-canvas-default-transparent); }', - fixed: '.x { border-color: var(--borderColor-transparent); }', - message: `Variable --color-canvas-default-transparent is deprecated for property border-color. Please use the replacement --borderColor-transparent. (primer/no-deprecated-colors)`, - line: 1, - column: 6, - }, - { - code: '.x { border: 1px solid var(--color-neutral-emphasis); .foo { background-color: var(--color-neutral-emphasis); } }', - fixed: - '.x { border: 1px solid var(--borderColor-neutral-emphasis); .foo { background-color: var(--bgColor-neutral-emphasis); } }', - config: [ - true, - { - inlineFallback: false, - }, - ], - warnings: [ - { - message: - 'Variable --color-neutral-emphasis is deprecated for property border. Please use the replacement --borderColor-neutral-emphasis. (primer/no-deprecated-colors)', - line: 1, - column: 6, - }, - { - message: - 'Variable --color-neutral-emphasis is deprecated for property background-color. Please use the replacement --bgColor-neutral-emphasis. (primer/no-deprecated-colors)', - line: 1, - column: 62, - }, - ], - }, - ], -}) -// eslint-disable-next-line no-undef -testRule({ - plugins: ['./plugins/no-deprecated-colors'], - ruleName, - config: [ - true, - { - inlineFallback: true, - }, - ], - fix: true, - accept: [ - {code: '.x { color: var(--fgColor-default, var(--color-fg-default)); }'}, - { - code: '@include focusOutline(2px, var(--focus-outlineColor, var(--color-accent-fg)));', - }, - ], - reject: [ - { - code: '.x { color: var(--color-fg-default); }', - fixed: '.x { color: var(--fgColor-default, var(--color-fg-default)); }', - message: `Variable --color-fg-default is deprecated for property color. Please use the replacement --fgColor-default. (primer/no-deprecated-colors)`, - line: 1, - column: 6, - }, - { - code: '.x { border-right: $border-width $border-style var(--color-border-muted); }', - fixed: '.x { border-right: $border-width $border-style var(--borderColor-muted, var(--color-border-muted)); }', - message: `Variable --color-border-muted is deprecated for property border-right. Please use the replacement --borderColor-muted. (primer/no-deprecated-colors)`, - line: 1, - column: 6, - }, - { - code: '.x { border-color: var(--color-primer-border-contrast); }', - fixed: '.x { border-color: var(--borderColor-muted, var(--color-primer-border-contrast)); }', - message: `Variable --color-primer-border-contrast is deprecated for property border-color. Please use the replacement --borderColor-muted. (primer/no-deprecated-colors)`, - line: 1, - column: 6, - }, - { - code: '.x { background-color: var(--color-canvas-default-transparent); }', - fixed: '.x { background-color: var(--bgColor-transparent, var(--color-canvas-default-transparent)); }', - message: `Variable --color-canvas-default-transparent is deprecated for property background-color. Please use the replacement --bgColor-transparent. (primer/no-deprecated-colors)`, - line: 1, - column: 6, - }, - { - code: '.x { border: var(--borderWidth-thin) solid var(--color-border-default); }', - fixed: '.x { border: var(--borderWidth-thin) solid var(--borderColor-default, var(--color-border-default)); }', - message: `Variable --color-border-default is deprecated for property border. Please use the replacement --borderColor-default. (primer/no-deprecated-colors)`, - line: 1, - column: 6, - }, - { - code: '.x { border-color: var(--color-canvas-default-transparent); }', - fixed: '.x { border-color: var(--borderColor-transparent, var(--color-canvas-default-transparent)); }', - message: `Variable --color-canvas-default-transparent is deprecated for property border-color. Please use the replacement --borderColor-transparent. (primer/no-deprecated-colors)`, - line: 1, - column: 6, - }, - { - code: '.x { border: 1px solid var(--color-neutral-emphasis); .foo { background-color: var(--color-neutral-emphasis); } }', - fixed: - '.x { border: 1px solid var(--borderColor-neutral-emphasis, var(--color-neutral-emphasis)); .foo { background-color: var(--bgColor-neutral-emphasis, var(--color-neutral-emphasis)); } }', - config: [ - true, - { - inlineFallback: false, - }, - ], - warnings: [ - { - message: - 'Variable --color-neutral-emphasis is deprecated for property border. Please use the replacement --borderColor-neutral-emphasis. (primer/no-deprecated-colors)', - line: 1, - column: 6, - }, - { - message: - 'Variable --color-neutral-emphasis is deprecated for property background-color. Please use the replacement --bgColor-neutral-emphasis. (primer/no-deprecated-colors)', - line: 1, - column: 62, - }, - ], - }, - ], -}) diff --git a/__tests__/no-override.js b/__tests__/no-override.js deleted file mode 100644 index dfcec5f9..00000000 --- a/__tests__/no-override.js +++ /dev/null @@ -1,114 +0,0 @@ -import {lint, extendDefaultConfig} from './utils/index.js' -import noOverride from '../plugins/no-override.js' - -describe('primer/no-override', () => { - it(`doesn't run when disabled`, () => { - const config = extendDefaultConfig({ - rules: { - 'primer/no-override': false, - }, - }) - return lint(`.ml-1 { width: 10px; }`, config).then(data => { - expect(data).not.toHaveErrored() - expect(data).toHaveWarningsLength(0) - }) - }) - - it('reports instances of utility classes', () => { - return lint('.ml-1 { width: 10px; }').then(data => { - expect(data).toHaveErrored() - expect(data).toHaveWarningsLength(1) - expect(data).toHaveWarnings([ - `Primer CSS class ".ml-1" should not be overridden. (primer/no-override)`, - ]) - }) - }) - - it('reports instances of complete utility selectors', () => { - const selector = '.show-on-focus:focus' - return lint(`${selector} { width: 10px; }`).then(data => { - expect(data).toHaveErrored() - expect(data).toHaveWarningsLength(1) - expect(data).toHaveWarnings([ - `Primer CSS class "${selector}" should not be overridden. (primer/no-override)`, - ]) - }) - }) - - it('reports instances of partial utility selectors', () => { - const selector = '.show-on-focus' - return lint(`.foo ${selector}:focus { width: 10px; }`).then(data => { - expect(data).toHaveErrored() - expect(data).toHaveWarningsLength(1) - expect(data).toHaveWarnings([ - `Primer CSS class "${selector}" should not be overridden in ".foo ${selector}:focus". (primer/no-override)`, - ]) - }) - }) - - it('only reports class selectors', () => { - const config = { - plugins: [noOverride], - rules: { - 'primer/no-override': [true], - }, - } - return lint(`body { width: 10px; }`, config).then(data => { - expect(data).not.toHaveErrored() - expect(data).toHaveWarningsLength(0) - }) - }) - - describe('ignoreSelectors option', () => { - it('ignores selectors listed as strings', () => { - const config = extendDefaultConfig({ - rules: { - 'primer/no-override': [ - true, - { - ignoreSelectors: ['.px-4'], - }, - ], - }, - }) - return lint(`.px-4 { margin: 0 $spacer-1 !important; }`, config).then(data => { - expect(data).not.toHaveErrored() - expect(data).toHaveWarningsLength(0) - }) - }) - - it('ignores selectors listed as regular expressions', () => { - const config = extendDefaultConfig({ - rules: { - 'primer/no-override': [ - true, - { - ignoreSelectors: [/\.px-[0-9]/], - }, - ], - }, - }) - return lint(`.px-4 { margin: 0 $spacer-1 !important; }`, config).then(data => { - expect(data).not.toHaveErrored() - expect(data).toHaveWarningsLength(0) - }) - }) - - it('ignores selectors when ignoreSelectors is a function', () => { - const config = extendDefaultConfig({ - rules: { - 'primer/no-override': [ - true, - { - ignoreSelectors: selector => selector === '.px-4', - }, - ], - }, - }) - return lint(`.px-4 { margin: 0 $spacer-1 !important; }`, config).then(data => { - expect(data).not.toHaveErrored() - expect(data).toHaveWarningsLength(0) - }) - }) - }) -}) diff --git a/__tests__/no-scale-colors.js b/__tests__/no-scale-colors.js deleted file mode 100644 index c2ec8ff3..00000000 --- a/__tests__/no-scale-colors.js +++ /dev/null @@ -1,39 +0,0 @@ -import path from 'path' -import {messages, ruleName} from '../plugins/no-scale-colors.js' -import {fileURLToPath} from 'url' - -const __dirname = fileURLToPath(new URL('.', import.meta.url)) - -// eslint-disable-next-line no-undef -testRule({ - plugins: ['./plugins/no-scale-colors'], - ruleName, - config: [ - true, - { - files: [path.join(__dirname, '__fixtures__/color-vars.scss')], - }, - ], - - accept: [ - {code: '.x { color: var(--color-text-primary); }'}, - { - code: '@include color-variables(((my-feature, (light: var(--color-scale-blue-1), dark: var(--color-scale-blue-5)))));', - }, - ], - - reject: [ - { - code: '.x { color: var(--color-scale-blue-1); }', - message: messages.rejected('--color-scale-blue-1'), - line: 1, - column: 6, - }, - { - code: '.x { color: var(--color-auto-blue-1); }', - message: messages.rejected('--color-auto-blue-1'), - line: 1, - column: 6, - }, - ], -}) diff --git a/__tests__/no-undefined-vars.js b/__tests__/no-undefined-vars.js deleted file mode 100644 index 0a1e5eae..00000000 --- a/__tests__/no-undefined-vars.js +++ /dev/null @@ -1,144 +0,0 @@ -import path from 'path' -import {messages, ruleName} from '../plugins/no-undefined-vars.js' -import {fileURLToPath} from 'url' - -const __dirname = fileURLToPath(new URL('.', import.meta.url)) - -// eslint-disable-next-line no-undef -testRule({ - plugins: ['./plugins/no-undefined-vars'], - ruleName, - config: [ - true, - { - files: [ - path.join(__dirname, '__fixtures__/color-vars.scss'), - path.join(__dirname, '__fixtures__/defines-new-color-vars.scss'), - path.join(__dirname, '__fixtures__/spacing-vars.scss'), - ], - }, - ], - - accept: [ - {code: '.x { color: var(--color-text-primary); }'}, - {code: '.x { color: var(--color-text-primary, #000000); }'}, - {code: '.x { background-color: var(--color-counter-bg); }'}, - {code: '.x { color: var(--color-my-first-feature); }'}, - {code: '.x { color: var(--color-my-second-feature); }'}, - {code: '.x { margin: var(--spacing-spacer-1); }'}, - { - code: '@include color-variables(\n (\n (feature-bg-color, (light: var(--color-scale-blue-1), dark: var(--color-scale-blue-2)))));', - }, - { - code: ` - .x { - --color-foo: #ffeeee; - } - `, - }, - { - code: ` - .x { - --color-foo: #ffeeee; - color: var(--color-foo); - } - `, - }, - { - code: ` - :root { - --color-foo: #ffeeee; - } - .x { - color: var(--color-foo); - } - `, - }, - { - code: ` - :host { - --color-foo: #ffeeee; - } - .x { - color: var(--color-foo); - } - `, - }, - { - code: ` - :root { - --color-foo: #ffeeee; - } - .x { - --color-bar: var(--color-foo); - color: var(--color-bar); - } - `, - }, - ], - - reject: [ - { - code: '.x { color: var(--color-foo); }', - message: messages.rejected('--color-foo'), - line: 1, - column: 6, - }, - // checks to ensure other declarations with a double-dash - // aren't accidentally parsed as CSS variables - { - code: '.x { color: var(--light); }', - message: messages.rejected('--light'), - line: 1, - column: 6, - }, - { - code: '.x { color: var(--color-my-commented-color); }', - message: messages.rejected('--color-my-commented-color'), - line: 1, - column: 6, - }, - { - code: '.x { color: var(--color-my-other-commented-color); }', - message: messages.rejected('--color-my-other-commented-color'), - line: 1, - column: 6, - }, - { - code: '.x { color: var(--color-bar, #000000); }', - message: messages.rejected('--color-bar'), - line: 1, - column: 6, - }, - { - code: '.x { --color-bar: #000000; } .y { color: var(--color-bar); }', - message: messages.rejected('--color-bar'), - line: 1, - column: 35, - }, - { - code: '.x { --color-foo: #000000; color: var(--color-bar); }', - message: messages.rejected('--color-bar'), - line: 1, - column: 28, - }, - { - code: ':root { --color-foo: #000000 } .x { color: var(--color-bar); }', - message: messages.rejected('--color-bar'), - line: 1, - column: 37, - }, - { - code: ':host { --color-foo: #000000 } .x { color: var(--color-bar); }', - message: messages.rejected('--color-bar'), - line: 1, - column: 37, - }, - { - code: '@include color-variables(\n (\n (feature-bg-color, (light: var(--color-scale-blue-1), dark: var(--color-fake-2)))));', - message: messages.rejected('--color-fake-2'), - line: 1, - column: 1, - }, - ], -}) diff --git a/__tests__/no-unused-vars.js b/__tests__/no-unused-vars.js deleted file mode 100644 index 281fb7b4..00000000 --- a/__tests__/no-unused-vars.js +++ /dev/null @@ -1,68 +0,0 @@ -import {join} from 'path' -import stylelint from 'stylelint' -import {fileURLToPath} from 'url' -import pluginPath from '../plugins/no-unused-vars.js' - -const __dirname = fileURLToPath(new URL('.', import.meta.url)) - -const fixture = (...path) => join(__dirname, '__fixtures__', ...path) -const ruleName = 'primer/no-unused-vars' - -describe('primer/no-unused-vars', () => { - it('finds unused vars', () => { - const config = { - plugins: [pluginPath], - customSyntax: 'postcss-scss', - rules: { - [ruleName]: [true, {files: fixture('*.scss')}], - }, - } - return stylelint - .lint({ - files: fixture('has-unused-vars.scss'), - config, - }) - .then(data => { - expect(data).toHaveErrored() - expect(data).toHaveWarnings([`The variable "$unused" is not referenced. (${ruleName})`]) - }) - }) - - it(`doesn't run when disabled`, () => { - const config = { - plugins: [pluginPath], - customSyntax: 'postcss-scss', - rules: { - [ruleName]: false, - }, - } - return stylelint - .lint({ - files: fixture('has-unused-vars.scss'), - config, - }) - .then(data => { - expect(data).not.toHaveErrored() - expect(data).toHaveWarningsLength(0) - }) - }) - - it(`talks a lot with {verbose: true}`, () => { - const config = { - plugins: [pluginPath], - customSyntax: 'postcss-scss', - rules: { - [ruleName]: [true, {files: fixture('*.scss'), verbose: true}], - }, - } - return stylelint - .lint({ - files: fixture('has-unused-vars.scss'), - config, - }) - .then(data => { - expect(data).toHaveErrored() - expect(data).toHaveWarnings([`The variable "$unused" is not referenced. (${ruleName})`]) - }) - }) -}) diff --git a/index.js b/index.js index 4d84c7c0..77413a63 100644 --- a/index.js +++ b/index.js @@ -4,16 +4,10 @@ import propertyOrder from './property-order.js' import borders from './plugins/borders.js' import boxShadow from './plugins/box-shadow.js' import colors from './plugins/colors.js' -import noDeprecatedColors from './plugins/no-deprecated-colors.js' -import noOverride from './plugins/no-override.js' -import noScaleColors from './plugins/no-scale-colors.js' -import noUndefinedVars from './plugins/no-undefined-vars.js' -import noUnusedVars from './plugins/no-unused-vars.js' import responsiveWidths from './plugins/responsive-widths.js' import spacing from './plugins/spacing.js' import typography from './plugins/typography.js' import utilities from './plugins/utilities.js' -import newColorVarsHaveFallback from './plugins/new-color-vars-have-fallback.js' import noDisplayColors from './plugins/no-display-colors.js' /** @type {import('stylelint').Config} */ @@ -22,21 +16,16 @@ export default { ignoreFiles: ['**/*.js', '**/*.cjs'], reportNeedlessDisables: true, plugins: [ + 'stylelint-value-no-unknown-custom-properties', 'stylelint-no-unsupported-browser-features', 'stylelint-order', borders, boxShadow, colors, - noDeprecatedColors, - noOverride, - noScaleColors, - noUndefinedVars, - noUnusedVars, responsiveWidths, spacing, typography, utilities, - newColorVarsHaveFallback, noDisplayColors, ], rules: { @@ -48,6 +37,25 @@ export default { 'color-named': 'never', 'color-no-invalid-hex': true, 'comment-no-empty': null, + 'csstools/value-no-unknown-custom-properties': [ + true, + { + severity: 'warning', + importFrom: [ + './node_modules/@primer/primitives/dist/css/functional/size/size-coarse.css', + './node_modules/@primer/primitives/dist/css/functional/size/border.css', + './node_modules/@primer/primitives/dist/css/functional/size/size.css', + './node_modules/@primer/primitives/dist/css/functional/size/size-fine.css', + './node_modules/@primer/primitives/dist/css/functional/size/breakpoints.css', + './node_modules/@primer/primitives/dist/css/functional/size/viewport.css', + './node_modules/@primer/primitives/dist/css/functional/motion/motion.css', + './node_modules/@primer/primitives/dist/css/functional/themes/light.css', + './node_modules/@primer/primitives/dist/css/functional/typography/typography.css', + './node_modules/@primer/primitives/dist/css/base/size/size.css', + './node_modules/@primer/primitives/dist/css/base/typography/typography.css', + ], + }, + ], 'custom-property-pattern': null, 'declaration-block-no-duplicate-properties': [true, {ignore: ['consecutive-duplicates']}], 'declaration-block-no-redundant-longhand-properties': null, @@ -82,13 +90,6 @@ export default { 'primer/borders': true, 'primer/box-shadow': true, 'primer/colors': true, - 'primer/no-deprecated-colors': true, - 'primer/no-override': true, - 'primer/no-undefined-vars': [ - true, - {severity: 'warning', files: 'node_modules/@primer/primitives/dist/scss/colors*/*.scss'}, - ], - 'primer/no-unused-vars': [true, {severity: 'warning'}], 'primer/responsive-widths': true, 'primer/spacing': true, 'primer/typography': true, @@ -141,13 +142,6 @@ export default { 'primer/borders': null, 'primer/typography': null, 'primer/box-shadow': null, - 'primer/no-deprecated-colors': [ - true, - { - inlineFallback: true, - }, - ], - 'primer/no-scale-colors': true, 'primer/no-display-colors': true, 'primer/utilities': null, 'property-no-unknown': [ @@ -156,7 +150,6 @@ export default { ignoreProperties: ['@container', 'container-type'], }, ], - 'primer/no-override': null, }, }, { @@ -177,14 +170,11 @@ export default { ignoreAtRules: ['mixin', 'define-mixin'], }, ], - 'primer/no-deprecated-colors': true, }, }, { files: ['**/*.module.css'], - rules: { - 'primer/no-override': null, - }, + rules: {}, }, ], } diff --git a/package-lock.json b/package-lock.json index 7eb978b0..c613a611 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "@primer/css": "^21.0.8", "@primer/primitives": "^7.16.0", "anymatch": "^3.1.1", - "globby": "^11.0.1", "postcss-scss": "^4.0.2", "postcss-styled-syntax": "^0.6.4", "postcss-value-parser": "^4.0.2", @@ -23,6 +22,7 @@ "stylelint-no-unsupported-browser-features": "^8.0.0", "stylelint-order": "^6.0.4", "stylelint-scss": "^6.2.0", + "stylelint-value-no-unknown-custom-properties": "^6.0.1", "tap-map": "^1.0.0" }, "devDependencies": { @@ -5947,7 +5947,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -6207,7 +6206,6 @@ "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, "dependencies": { "hasown": "^2.0.0" }, @@ -8101,7 +8099,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, "node_modules/path-scurry": { @@ -8682,7 +8679,6 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -9565,6 +9561,21 @@ "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.29.0.tgz", "integrity": "sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==" }, + "node_modules/stylelint-value-no-unknown-custom-properties": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/stylelint-value-no-unknown-custom-properties/-/stylelint-value-no-unknown-custom-properties-6.0.1.tgz", + "integrity": "sha512-N60PTdaTknB35j6D4FhW0GL2LlBRV++bRpXMMldWMQZ240yFQaoltzlLY4lXXs7Z0J5mNUYZQ/gjyVtU2DhCMA==", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "resolve": "^1.22.8" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "stylelint": ">=16" + } + }, "node_modules/stylelint/node_modules/ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", @@ -9675,7 +9686,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" diff --git a/package.json b/package.json index 160b4b9a..b568d821 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "@primer/css": "^21.0.8", "@primer/primitives": "^7.16.0", "anymatch": "^3.1.1", - "globby": "^11.0.1", "postcss-scss": "^4.0.2", "postcss-styled-syntax": "^0.6.4", "postcss-value-parser": "^4.0.2", @@ -57,6 +56,7 @@ "stylelint-no-unsupported-browser-features": "^8.0.0", "stylelint-order": "^6.0.4", "stylelint-scss": "^6.2.0", + "stylelint-value-no-unknown-custom-properties": "^6.0.1", "tap-map": "^1.0.0" }, "prettier": "@github/prettier-config", diff --git a/plugins/README.md b/plugins/README.md index 7e90b391..72f08597 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -7,11 +7,6 @@ This directory contains all of our custom stylelint plugins, each of which provi - [Primer stylelint plugins](#primer-stylelint-plugins) - [Rules](#rules) - [Usage](#usage) - - [`primer/no-override`](#primerno-override) - - [`primer/no-unused-vars`](#primerno-unused-vars) - - [`primer/no-deprecated-colors`](#primerno-deprecated-colors) - - [`primer/no-undefined-vars`](#primerno-undefined-vars) - - [`primer/no-scale-colors`](#primerno-scale-colors) - [`primer/colors`](#primercolors) - [`primer/spacing`](#primerspacing) - [`primer/typography`](#primertypography) @@ -33,131 +28,7 @@ If you're _not_ using or extending `@primer/stylelint-config`, you can still ref ```js // stylelint.config.js module.exports = { - plugins: ['@primer/stylelint-config/plugins/no-override', '@primer/stylelint-config/plugins/no-unused-vars'] -} -``` - -## `primer/no-override` - -This rule prohibits "overriding" class selectors defined in [Primer CSS]. By default, it will only fail selectors that target utility classes: - -```scss -// FAIL -.mt-0 { - /* literally anything */ -} -``` - -You can further constrain overrides to exclude _any_ class selector in Primer by providing additional names in the `bundles` option: - -```js -// stylelint.config.js -module.exports = { - // ... - rules: { - 'primer/no-override': [ - true, - { - bundles: ['utilities', core', 'product', 'marketing'] - } - ] - } -} -``` - -## `primer/no-unused-vars` - -This rule helps you find SCSS variables that _may_ not be used, and can be safely deleted. It works by scanning all of the SCSS files in your project and looking for anything that appears to be either a Sass variable declaration or reference: - -```scss - $name: value; -/** ↑ - * The colon is what makes this a declaration */ - -/* Anything starting with a $ and followed by word chars or hyphens - * and _not_ followed by a colon is considered a reference: */ - - margin: $value; -/** ↑ - * Not a colon */ - padding: $value 1px; -/** ↑ - * Not a colon */ - -@media screen and (max-width: $break-lg) { -/** ↑ - * Also not a colon */ -``` - -Equipped with a list of all the variable declarations and references, the linting rule walks all of the declarations in the -file being linted, finds any that look like declarations (using the same pattern as the project-wide scan), and generates a warning for any that have zero references in the files it's scanned. - -Because there isn't any good way for a stylelint plugin to know all of the files being linted, it needs to be told where to find all of the declarations and references in its options: - -- `files` is a single path, glob, or array of paths and globs, that tells the plugin which files to scan relative to the current working directory. The default is `['**/*.scss', '!node_modules']`, which tells [globby] to find all the `.scss` files recursively and ignore the `node_modules` directory. - -- `variablePattern` is a regular expression that matches a single variable in either a source file string or the `prop` of a postcss Declaration node (`{type: 'decl'}`). The default matches Sass/SCSS variables: `/\$[-\w]/g`. Note that the `g` ("global") flag is _required_ to match multiple variable references on a single line. - -- `verbose` is a boolean that enables chatty `console.warn()` messages telling you what the plugin found, which can aid in debugging more complicated project layouts. - -## `primer/no-deprecated-colors` - -This rule identifies deprecated color variables from [primer/primitives]](https://github.com/primer/primitives) deprecated.json file and suggests replacements. - -```scss -body { - color: var(--color-fg-default); -} -/** ↑ - * OK: --color-text-primary is defined */ - -body { - color: var(--color-text-primary); -} -/** ↑ - * FAIL: --color-text-primary is deprecated. */ -``` - -## `primer/no-undefined-vars` - -This rule prohibits any usages of undefined CSS variables. - -```scss -:root { - --color-text-primary: #000; -} - -body { - color: var(--color-text-primary); -} -/** ↑ - * OK: --color-text-primary is defined */ - -body { - color: var(--color-foo); -} -/** ↑ - * FAIL: --color-foo is not defined */ -``` - -For the purposes of this rule, a CSS variable declaration is any text starting with `--` and immediately followed by a colon. - -Because there isn't a good way for a stylelint plugin to know what CSS variables are defined, it needs to be told where to look for declarations in its options: - -- `files` is a single path, glob, or array of paths and globs, that tells the plugin which files (relative to the current working directory) to scan for CSS variable declarations. The default is `['**/*.scss', '!node_modules']`, which tells [globby] to find all the `.scss` files recursively and ignore the `node_modules` directory. -- `verbose` is a boolean that enables chatty `console.warn()` messages telling you what the plugin found, which can aid in debugging more complicated project layouts. - -## `primer/no-scale-colors` - -This rule prohibits the use of [non-functional scale CSS variables](https://primer.style/css/support/color-system#color-palette) like `var(--color-scale-blue-1)` in all cases except the `color-variables` mixin. - -```scss -// Okay; using scale colors while defining new variables -@include color-scale-var('new-var-name', var(--color-scale-blue-1), var(--color-scale-blue-2)) - -// Fail; using scale colors directly as a property value -.selector { - color: var(--color-scale-blue-1) + plugins: ['@primer/stylelint-config/plugins/colors'] } ``` @@ -377,5 +248,4 @@ module.exports = { - `disableFix` is a boolean that can disable auto-fixing of this rule when running `stylelint --fix` to auto-fix other rules. [primer css]: https://primer.style/css -[globby]: https://www.npmjs.com/package/globby [glob patterns]: http://tldp.org/LDP/GNU-Linux-Tools-Summary/html/x11655.htm diff --git a/plugins/new-color-vars-have-fallback.js b/plugins/new-color-vars-have-fallback.js deleted file mode 100644 index ccd8cd77..00000000 --- a/plugins/new-color-vars-have-fallback.js +++ /dev/null @@ -1,35 +0,0 @@ -import stylelint from 'stylelint' -import variables from './lib/new-color-css-vars-map.json' with {type: 'json'} - -export const ruleName = 'primer/new-color-vars-have-fallback' -export const messages = stylelint.utils.ruleMessages(ruleName, { - expectedFallback: variable => - `Expected a fallback value for CSS variable ${variable}. New color variables fallbacks, check primer.style/primitives to find the correct value`, -}) - -export default stylelint.createPlugin(ruleName, enabled => { - if (!enabled) { - return noop - } - - return (root, result) => { - root.walkDecls(node => { - for (const variable of variables) { - if (node.value.includes(`var(${variable})`)) { - // Check if the declaration uses a CSS variable from the JSON - const match = node.value.match(new RegExp(`var\\(${variable},(.*)\\)`)) - if (!match || match[1].trim() === '') { - stylelint.utils.report({ - ruleName, - result, - node, - message: messages.expectedFallback(variable), - }) - } - } - } - }) - } -}) - -function noop() {} diff --git a/plugins/no-deprecated-colors.js b/plugins/no-deprecated-colors.js deleted file mode 100644 index f783f680..00000000 --- a/plugins/no-deprecated-colors.js +++ /dev/null @@ -1,97 +0,0 @@ -import stylelint from 'stylelint' -import matchAll from 'string.prototype.matchall' -import variableChecks from './lib/primitives-v8.json' with {type: 'json'} -export const ruleName = 'primer/no-deprecated-colors' -export const messages = stylelint.utils.ruleMessages(ruleName, { - rejected: (varName, replacement, property) => { - if (replacement === null) { - return `Variable ${varName} is deprecated for property ${property}. Please consult the primer color docs for a replacement. https://primer.style/primitives/storybook/?path=/story/migration-tables` - } - - return `Variable ${varName} is deprecated for property ${property}. Please use the replacement ${replacement}.` - }, -}) - -// Match CSS variable references (e.g var(--color-text-primary)) -// eslint-disable-next-line no-useless-escape -const variableReferenceRegex = /var\(([^\),]+)(,.*)?\)/g - -const replacedVars = {} -const newVars = {} - -export default stylelint.createPlugin(ruleName, (enabled, options = {}, context) => { - const {inlineFallback = false} = options - - if (!enabled) { - return noop - } - - const {verbose = false} = options - // eslint-disable-next-line no-console - const log = verbose ? (...args) => console.warn(...args) : noop - - // Keep track of declarations we've already seen - const seen = new WeakMap() - - const lintResult = (root, result) => { - // Walk all declarations - root.walk(node => { - if (seen.has(node)) { - return - } else { - seen.set(node, true) - } - - // walk these nodes - if (node.type !== 'decl') { - return - } - - for (const [, variableName] of matchAll(node.value, variableReferenceRegex)) { - if (variableName in variableChecks) { - let replacement = variableChecks[variableName] - if (typeof replacement === 'object') { - if (node.prop) { - for (const prop of replacement) { - // Check if node.prop starts with one of the props array elements - if (prop['props'].some(p => node.prop.startsWith(p))) { - replacement = prop['replacement'] - break - } - } - } - if (typeof replacement === 'object') { - replacement = null - } - } - - if (context.fix && replacement !== null) { - replacement = `${replacement}${inlineFallback ? `, var(${variableName})` : ''}` - replacedVars[variableName] = true - newVars[replacement] = true - if (node.type === 'atrule') { - node.params = node.params.replace(variableName, replacement) - } else { - node.value = node.value.replace(variableName, replacement) - } - continue - } - - stylelint.utils.report({ - message: messages.rejected(variableName, replacement, node.prop), - node, - ruleName, - result, - }) - } - } - }) - } - - log( - `${Object.keys(replacedVars).length} deprecated variables replaced with ${Object.keys(newVars).length} variables.`, - ) - return lintResult -}) - -function noop() {} diff --git a/plugins/no-override.js b/plugins/no-override.js deleted file mode 100644 index 787a92c9..00000000 --- a/plugins/no-override.js +++ /dev/null @@ -1,98 +0,0 @@ -import stylelint from 'stylelint' -import primerJson from '@primer/css/dist/stats/primer.json' with {type: 'json'} - -const ruleName = 'primer/no-override' -const CLASS_PATTERN = /(\.[-\w]+)/ -const CLASS_PATTERN_ALL = new RegExp(CLASS_PATTERN, 'g') -const CLASS_PATTERN_ONLY = /^\.[-\w]+(:{1,2}[-\w]+)?$/ - -export default stylelint.createPlugin(ruleName, (enabled, options = {}) => { - if (!enabled) { - return noop - } - - const {ignoreSelectors = []} = options - - const isSelectorIgnored = - typeof ignoreSelectors === 'function' - ? ignoreSelectors - : selector => { - return ignoreSelectors.some(pattern => { - return pattern instanceof RegExp ? pattern.test(selector) : selector.includes(pattern) - }) - } - - // These map selectors to the bundle in which they're defined. - // If there's no entry for a given selector, it means that it's not defined - // in one of the *specified* bundles, since we're iterating over the list of - // bundle names in the options. - const immutableSelectors = new Set() - const immutableClassSelectors = new Set() - - const selectors = primerJson.selectors.values - for (const selector of selectors) { - immutableSelectors.add(selector) - for (const classSelector of getClassSelectors(selector)) { - immutableClassSelectors.add(classSelector) - } - } - - const messages = stylelint.utils.ruleMessages(ruleName, { - rejected: ({rule, selector}) => { - const ruleSelector = collapseWhitespace(rule.selector) - const context = selector === rule.selector ? '' : ` in "${ruleSelector}"` - return `Primer CSS class "${collapseWhitespace(selector)}" should not be overridden${context}.` - }, - }) - - return (root, result) => { - const report = subject => - stylelint.utils.report({ - message: messages.rejected(subject), - node: subject.rule, - result, - ruleName, - }) - - root.walkRules(rule => { - const {selector} = rule - if (immutableSelectors.has(selector)) { - if (isClassSelector(selector)) { - if (!isSelectorIgnored(selector)) { - return report({ - rule, - selector, - }) - } - } else { - // console.log(`not a class selector: "${selector}"`) - } - } - for (const classSelector of getClassSelectors(selector)) { - if (immutableClassSelectors.has(classSelector)) { - if (!isSelectorIgnored(classSelector)) { - return report({ - rule, - selector: classSelector, - }) - } - } - } - }) - } -}) - -function getClassSelectors(selector) { - const match = selector.match(CLASS_PATTERN_ALL) - return match ? [...match] : [] -} - -function isClassSelector(selector) { - return CLASS_PATTERN_ONLY.test(selector) -} - -function collapseWhitespace(str) { - return str.trim().replace(/\s+/g, ' ') -} - -function noop() {} diff --git a/plugins/no-scale-colors.js b/plugins/no-scale-colors.js deleted file mode 100644 index 77891962..00000000 --- a/plugins/no-scale-colors.js +++ /dev/null @@ -1,51 +0,0 @@ -import stylelint from 'stylelint' -import matchAll from 'string.prototype.matchall' - -export const ruleName = 'primer/no-scale-colors' -export const messages = stylelint.utils.ruleMessages(ruleName, { - rejected: varName => - `${varName} is a non-functional scale color and cannot be used without being wrapped in the color-variables mixin`, -}) - -// Match CSS variable references (e.g var(--color-text-primary)) -// eslint-disable-next-line no-useless-escape -const variableReferenceRegex = /var\(([^\),]+)(,.*)?\)/g - -export default stylelint.createPlugin(ruleName, (enabled, options = {}) => { - if (!enabled) { - return noop - } - - const {verbose = false} = options - // eslint-disable-next-line no-console - const log = verbose ? (...args) => console.warn(...args) : noop - - // Keep track of declarations we've already seen - const seen = new WeakMap() - - return (root, result) => { - root.walkRules(rule => { - rule.walkDecls(decl => { - if (seen.has(decl)) { - return - } else { - seen.set(decl, true) - } - - for (const [, variableName] of matchAll(decl.value, variableReferenceRegex)) { - log(`Found variable reference ${variableName}`) - if (variableName.match(/^--color-(scale|auto)-/)) { - stylelint.utils.report({ - message: messages.rejected(variableName), - node: decl, - result, - ruleName, - }) - } - } - }) - }) - } -}) - -function noop() {} diff --git a/plugins/no-undefined-vars.js b/plugins/no-undefined-vars.js deleted file mode 100644 index 9c36f242..00000000 --- a/plugins/no-undefined-vars.js +++ /dev/null @@ -1,118 +0,0 @@ -import fs from 'fs' -import stylelint from 'stylelint' -import matchAll from 'string.prototype.matchall' -import globby from 'globby' -import TapMap from 'tap-map' - -export const ruleName = 'primer/no-undefined-vars' -export const messages = stylelint.utils.ruleMessages(ruleName, { - rejected: varName => `${varName} is not defined`, -}) - -// Match CSS variable definitions (e.g. --color-text-primary:) -const variableDefinitionRegex = /^\s*(--[\w|-]+):/gm - -// Match CSS variables defined with the color-variables mixin -const colorModeVariableDefinitionRegex = /^[^/\n]*\(["']?([^'"\s,]+)["']?,\s*\(light|dark:/gm - -// Match CSS variable references (e.g var(--color-text-primary)) -// eslint-disable-next-line no-useless-escape -const variableReferenceRegex = /var\(([^\),]+)(,.*)?\)/g - -export default stylelint.createPlugin(ruleName, (enabled, options = {}) => { - if (!enabled) { - return noop - } - - const {files = ['**/*.scss', '!node_modules'], verbose = false} = options - // eslint-disable-next-line no-console - const log = verbose ? (...args) => console.warn(...args) : noop - const globalDefinedVariables = getDefinedVariables(files, log) - // Keep track of declarations we've already seen - const seen = new WeakMap() - - return (root, result) => { - const fileDefinedVariables = new Set() - - function checkVariable(variableName, node, allowed) { - if (!allowed.has(variableName)) { - stylelint.utils.report({ - message: messages.rejected(variableName), - node, - result, - ruleName, - }) - } - } - - root.walkAtRules(rule => { - if (rule.name === 'include' && rule.params.startsWith('color-variables')) { - const innerMatch = [...matchAll(rule.params, variableReferenceRegex)] - if (!innerMatch.length) { - return - } - - for (const [, variableName] of innerMatch) { - checkVariable(variableName, rule, new Set([...globalDefinedVariables, ...fileDefinedVariables])) - } - } - }) - - root.walkRules(rule => { - const scopeDefinedVaribles = new Set() - - rule.walkDecls(decl => { - // Add CSS variable declarations within the source text to the list of allowed variables - if (decl.prop.startsWith('--')) { - scopeDefinedVaribles.add(decl.prop) - if (decl.parent.selector === ':root' || decl.parent.selector === ':host') { - fileDefinedVariables.add(decl.prop) - } - } - - if (seen.has(decl)) { - return - } else { - seen.set(decl, true) - } - - for (const [, variableName] of matchAll(decl.value, variableReferenceRegex)) { - checkVariable( - variableName, - decl, - new Set([...globalDefinedVariables, ...fileDefinedVariables, ...scopeDefinedVaribles]), - ) - } - }) - }) - } -}) - -const cwd = process.cwd() -const cache = new TapMap() - -function getDefinedVariables(globs, log) { - const cacheKey = JSON.stringify({globs, cwd}) - return cache.tap(cacheKey, () => { - const definedVariables = new Set() - - const files = globby.sync(globs) - log(`Scanning ${files.length} SCSS files for CSS variables`) - for (const file of files) { - log(`==========\nLooking for CSS variable definitions in ${file}`) - const css = fs.readFileSync(file, 'utf-8') - for (const [, variableName] of matchAll(css, variableDefinitionRegex)) { - log(`${variableName} defined in ${file}`) - definedVariables.add(variableName) - } - for (const [, variableName] of matchAll(css, colorModeVariableDefinitionRegex)) { - log(`--color-${variableName} defined via color-variables in ${file}`) - definedVariables.add(`--color-${variableName}`) - } - } - - return definedVariables - }) -} - -function noop() {} diff --git a/plugins/no-unused-vars.js b/plugins/no-unused-vars.js deleted file mode 100644 index f19efb43..00000000 --- a/plugins/no-unused-vars.js +++ /dev/null @@ -1,96 +0,0 @@ -import TapMap from 'tap-map' -import globby from 'globby' -import matchAll from 'string.prototype.matchall' -import stylelint from 'stylelint' -import {readFileSync} from 'fs' - -export const ruleName = 'primer/no-unused-vars' - -const cwd = process.cwd() -const COLON = ':' -const SCSS_VARIABLE_PATTERN = /(\$[-\w]+)/g - -export const messages = stylelint.utils.ruleMessages(ruleName, { - rejected: name => `The variable "${name}" is not referenced.`, -}) - -const cache = new TapMap() - -export default stylelint.createPlugin(ruleName, (enabled, options = {}) => { - if (!enabled) { - return noop - } - - const {files = ['**/*.scss', '!node_modules'], variablePattern = SCSS_VARIABLE_PATTERN, verbose = false} = options - // eslint-disable-next-line no-console - const log = verbose ? (...args) => console.warn(...args) : noop - const cacheOptions = {files, variablePattern, cwd} - const {refs} = getCachedVariables(cacheOptions, log) - - return (root, result) => { - root.walkDecls(decl => { - for (const [name] of matchAll(decl.prop, variablePattern)) { - if (!refs.has(name)) { - stylelint.utils.report({ - message: messages.rejected(name), - node: decl, - result, - ruleName, - }) - } else { - const path = stripCwd(decl.source.input.file) - log(`${name} declared in ${path} ref'd in ${pluralize(refs.get(name).size, 'file')}`) - } - } - }) - } -}) - -function getCachedVariables(options, log) { - const key = JSON.stringify(options) - return cache.tap(key, () => { - const {files, variablePattern} = options - const decls = new TapMap() - const refs = new TapMap() - - log(`Looking for variables in ${files} ...`) - for (const file of globby.sync(files)) { - const css = readFileSync(file, 'utf8') - for (const match of matchAll(css, variablePattern)) { - const after = css.substr(match.index + match[0].length) - const name = match[0] - if (after.startsWith(COLON)) { - decls.tap(name, set).add(file) - } else { - refs.tap(name, set).add(file) - } - } - } - log(`Found ${decls.size} declarations, ${pluralize(refs.size, 'reference')}.`) - - for (const [name, filesList] of decls.entries()) { - const fileRefs = refs.get(name) - if (fileRefs) { - log(`variable "${name}" declared in ${pluralize(filesList.size, 'file')}, ref'd in ${fileRefs.size}`) - } else { - log(`[!] variable "${name}" declared in ${Array.from(filesList)[0]} is not referenced`) - } - } - - return {decls, refs} - }) -} - -function noop() {} - -function set() { - return new Set() -} - -function stripCwd(path) { - return path.startsWith(cwd) ? path.substr(cwd.length + 1) : path -} - -function pluralize(num, str, plural = `${str}s`) { - return num === 1 ? `${num} ${str}` : `${num} ${plural}` -}