diff --git a/.changeset/short-suits-knock.md b/.changeset/short-suits-knock.md new file mode 100644 index 0000000000..b6b84a7f22 --- /dev/null +++ b/.changeset/short-suits-knock.md @@ -0,0 +1,5 @@ +--- +"stylelint": patch +--- + +Fixed: `custom-property-empty-line-before` false positives for CSS-in-JS diff --git a/lib/__tests__/fixtures/postcss-naive-css-in-js.js b/lib/__tests__/fixtures/postcss-naive-css-in-js.js new file mode 100644 index 0000000000..4ebf18c14b --- /dev/null +++ b/lib/__tests__/fixtures/postcss-naive-css-in-js.js @@ -0,0 +1,34 @@ +const postcss = require('postcss'); + +/** + * @type {postcss.Parser} + */ +function parse(css) { + const source = typeof css === 'string' ? css : css.toString(); + + const document = postcss.document({ + source: { + input: new postcss.Input(source), + }, + }); + + // E.g. "css` color: red; `;" + for (const match of source.matchAll(/\bcss`([^`]+)`;/g)) { + document.append(postcss.parse(match[1])); + } + + return document; +} + +/** + * @type {postcss.Stringifier} + */ +function stringify(node, builder) { + if (node.type === 'document') { + node.each((root) => { + builder(`css\`${root}\`;`, root); + }); + } +} + +module.exports = { parse, stringify }; diff --git a/lib/rules/custom-property-empty-line-before/__tests__/index.js b/lib/rules/custom-property-empty-line-before/__tests__/index.js index f48906acd9..91de21b81a 100644 --- a/lib/rules/custom-property-empty-line-before/__tests__/index.js +++ b/lib/rules/custom-property-empty-line-before/__tests__/index.js @@ -1,5 +1,7 @@ 'use strict'; +const naiveCssInJs = require('../../../__tests__/fixtures/postcss-naive-css-in-js'); + const { messages, ruleName } = require('..'); testRule({ @@ -570,3 +572,15 @@ testRule({ }, ], }); + +testRule({ + ruleName, + config: ['always', { except: ['first-nested'] }], + customSyntax: naiveCssInJs, + + accept: [ + { + code: 'css` --foo: 100px; `;', + }, + ], +}); diff --git a/lib/utils/__tests__/isFirstNested.test.js b/lib/utils/__tests__/isFirstNested.test.js index 6e699bc0a4..8c09bb64bb 100644 --- a/lib/utils/__tests__/isFirstNested.test.js +++ b/lib/utils/__tests__/isFirstNested.test.js @@ -161,4 +161,26 @@ describe('isFirstNested', () => { expect(isFirstNested(decls[2])).toBe(false); expect(isFirstNested(decls[3])).toBe(false); }); + + it('returns false without a parent', () => { + const decl = postcss.decl({ prop: 'color', value: 'pink' }); + + expect(isFirstNested(decl)).toBe(false); + }); + + it('returns true with the first-nested declaration in a document', () => { + const document = postcss.document(); + + document.append(postcss.parse('color: pink;')); + document.append(postcss.parse('color: red; color: blue;')); + + const decls = []; + + document.walkDecls('color', (decl) => decls.push(decl)); + + expect(decls).toHaveLength(3); + expect(isFirstNested(decls[0])).toBe(true); + expect(isFirstNested(decls[1])).toBe(true); + expect(isFirstNested(decls[2])).toBe(false); + }); }); diff --git a/lib/utils/isFirstNested.js b/lib/utils/isFirstNested.js index 2aa3788e00..56c55275c3 100644 --- a/lib/utils/isFirstNested.js +++ b/lib/utils/isFirstNested.js @@ -1,6 +1,6 @@ 'use strict'; -const { isComment, hasSource } = require('./typeGuards'); +const { isComment, isDocument, isRoot, hasSource } = require('./typeGuards'); /** * @param {import('postcss').Node} statement @@ -9,7 +9,11 @@ const { isComment, hasSource } = require('./typeGuards'); module.exports = function isFirstNested(statement) { const parentNode = statement.parent; - if (parentNode === undefined || parentNode.type === 'root') { + if (parentNode === undefined) { + return false; + } + + if (isRoot(parentNode) && !isInDocument(parentNode)) { return false; } @@ -71,3 +75,11 @@ module.exports = function isFirstNested(statement) { /* istanbul ignore next: Should always return in the loop */ return false; }; + +/** + * @param {import('postcss').Node} node + * @returns {boolean} + */ +function isInDocument({ parent }) { + return Boolean(parent && isDocument(parent)); +} diff --git a/lib/utils/typeGuards.js b/lib/utils/typeGuards.js index 2bf6a4624e..705cccef70 100644 --- a/lib/utils/typeGuards.js +++ b/lib/utils/typeGuards.js @@ -1,7 +1,7 @@ 'use strict'; /** @typedef {import('postcss').Node} Node */ -/** @typedef {import('postcss').Node} NodeSource */ +/** @typedef {import('postcss').Source} NodeSource */ /** * @param {Node} node @@ -43,6 +43,14 @@ module.exports.isDeclaration = function isDeclaration(node) { return node.type === 'decl'; }; +/** + * @param {Node} node + * @returns {node is import('postcss').Document} + */ +module.exports.isDocument = function isDocument(node) { + return node.type === 'document'; +}; + /** * @param {import('postcss-value-parser').Node} node * @returns {node is import('postcss-value-parser').FunctionNode}