Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix custom-property-empty-line-before false positives for CSS-in-JS #6767

Merged
merged 3 commits into from Apr 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/short-suits-knock.md
@@ -0,0 +1,5 @@
---
"stylelint": patch
---

Fixed: `custom-property-empty-line-before` false positives for CSS-in-JS
34 changes: 34 additions & 0 deletions lib/__tests__/fixtures/postcss-naive-css-in-js.js
Copy link
Member Author

@ybiquitous ybiquitous Apr 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[note] Instead of installing a custom syntax package, I wrote a naive custom syntax only for testing.
See also https://postcss.org/docs/how-to-write-custom-syntax

@@ -0,0 +1,34 @@
const postcss = require('postcss');

/**
* @type {postcss.Parser<postcss.Document>}
*/
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 };
14 changes: 14 additions & 0 deletions 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({
Expand Down Expand Up @@ -570,3 +572,15 @@ testRule({
},
],
});

testRule({
ruleName,
config: ['always', { except: ['first-nested'] }],
customSyntax: naiveCssInJs,

accept: [
{
code: 'css` --foo: 100px; `;',
},
],
});
22 changes: 22 additions & 0 deletions lib/utils/__tests__/isFirstNested.test.js
Expand Up @@ -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);
});
});
16 changes: 14 additions & 2 deletions 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
Expand All @@ -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;
}

Expand Down Expand Up @@ -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));
}
10 changes: 9 additions & 1 deletion 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
Expand Down Expand Up @@ -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}
Expand Down