Skip to content

Commit

Permalink
Add media-query-no-invalid specific problem messages (#7462)
Browse files Browse the repository at this point in the history
* Add `media-query-no-invalid` specific problem messages

* Create healthy-donkeys-appear.md

* apply suggestions

* Update .changeset/healthy-donkeys-appear.md

Co-authored-by: Masafumi Koba <473530+ybiquitous@users.noreply.github.com>

---------

Co-authored-by: Masafumi Koba <473530+ybiquitous@users.noreply.github.com>
  • Loading branch information
romainmenke and ybiquitous committed Jan 15, 2024
1 parent 5f5cada commit 5bb517b
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 64 deletions.
5 changes: 5 additions & 0 deletions .changeset/healthy-donkeys-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"stylelint": minor
---

Added: `media-query-no-invalid` specific problem messages
30 changes: 24 additions & 6 deletions lib/rules/media-query-no-invalid/__tests__/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -159,31 +159,43 @@ testRule({
},
{
code: '@media (--foo: 2) {}',
message: messages.rejected('(--foo: 2)'),
message: messages.rejected(
'(--foo: 2)',
'custom media queries can only be used in boolean queries',
),
line: 1,
column: 8,
endLine: 1,
endColumn: 18,
},
{
code: '@media (min-width < 500px) {}',
message: messages.rejected('(min-width < 500px)'),
message: messages.rejected(
'(min-width < 500px)',
'"min-" and "max-" prefixes are not needed when using range queries',
),
line: 1,
column: 8,
endLine: 1,
endColumn: 27,
},
{
code: '@media (min-width) {}',
message: messages.rejected('(min-width)'),
message: messages.rejected(
'(min-width)',
'"min-" and "max-" prefixes are not needed in boolean queries',
),
line: 1,
column: 8,
endLine: 1,
endColumn: 19,
},
{
code: '@media (grid < 0) {}',
message: messages.rejected('(grid < 0)'),
message: messages.rejected(
'(grid < 0)',
'discrete features can only be used in plain and boolean queries',
),
line: 1,
column: 8,
endLine: 1,
Expand Down Expand Up @@ -248,15 +260,21 @@ testRule({
},
{
code: '@media (--foo: 300px) {}',
message: messages.rejected('(--foo: 300px)'),
message: messages.rejected(
'(--foo: 300px)',
'custom media queries can only be used in boolean queries',
),
line: 1,
column: 8,
endLine: 1,
endColumn: 22,
},
{
code: '@media (--foo < 300px) {}',
message: messages.rejected('(--foo < 300px)'),
message: messages.rejected(
'(--foo < 300px)',
'custom media queries can only be used in boolean queries',
),
line: 1,
column: 8,
endLine: 1,
Expand Down
70 changes: 41 additions & 29 deletions lib/rules/media-query-no-invalid/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,20 @@ const validateOptions = require('../../utils/validateOptions.cjs');
const ruleName = 'media-query-no-invalid';

const messages = ruleMessages(ruleName, {
rejected: (query) => `Unexpected invalid media query "${query}"`,
rejected: (query, reason) => {
if (!reason) return `Unexpected invalid media query "${query}"`;

return `Unexpected invalid media query "${query}", ${reason}`;
},
});

const reasons = {
custom: 'custom media queries can only be used in boolean queries',
min_max_in_range: '"min-" and "max-" prefixes are not needed when using range queries',
min_max_in_boolean: '"min-" and "max-" prefixes are not needed in boolean queries',
discrete: 'discrete features can only be used in plain and boolean queries',
};

const HAS_MIN_MAX_PREFIX = /^(?:min|max)-/i;

const meta = {
Expand All @@ -34,21 +45,20 @@ const rule = (primary) => {
}

root.walkAtRules(/^media$/i, (atRule) => {
/** @type {Array<{tokens(): Array<import('@csstools/css-tokenizer').CSSToken>}>} */
let invalidNodes = [];
const atRuleParamIndexValue = atRuleParamIndex(atRule);

parseMediaQuery(atRule).forEach((mediaQuery) => {
if (mediaQueryListParser.isMediaQueryInvalid(mediaQuery)) {
// Queries that fail to parse are invalid.
invalidNodes.push(mediaQuery);
complain(atRule, atRuleParamIndexValue, mediaQuery);

return;
}

mediaQuery.walk(({ node, parent }) => {
// All general enclosed nodes are invalid.
if (mediaQueryListParser.isGeneralEnclosed(node)) {
invalidNodes.push(node);
complain(atRule, atRuleParamIndexValue, node);

return;
}
Expand All @@ -59,7 +69,7 @@ const rule = (primary) => {

if (isCustomMediaQuery(name)) {
// In a plain context, custom media queries are invalid.
invalidNodes.push(parent);
complain(atRule, atRuleParamIndexValue, parent, 'custom');

return;
}
Expand All @@ -73,21 +83,21 @@ const rule = (primary) => {

if (isCustomMediaQuery(name)) {
// In a range context, custom media queries are invalid.
invalidNodes.push(parent);
complain(atRule, atRuleParamIndexValue, parent, 'custom');

return;
}

if (HAS_MIN_MAX_PREFIX.test(name)) {
// In a range context, min- and max- prefixed feature names are invalid.
invalidNodes.push(parent);
complain(atRule, atRuleParamIndexValue, parent, 'min_max_in_range');

return;
}

if (!mediaFeatures.rangeTypeMediaFeatureNames.has(name)) {
// In a range context, non-range typed features are invalid.
invalidNodes.push(parent);
complain(atRule, atRuleParamIndexValue, parent, 'discrete');

return;
}
Expand All @@ -97,34 +107,36 @@ const rule = (primary) => {

// Invalid boolean media features.
if (mediaQueryListParser.isMediaFeatureBoolean(node)) {
const name = node.getName().toLowerCase();
const name = node.getName();

if (HAS_MIN_MAX_PREFIX.test(name)) {
// In a range context, min- and max- prefixed feature names are invalid
invalidNodes.push(parent);
// In a boolean feature, min- and max- prefixed feature names are invalid
complain(atRule, atRuleParamIndexValue, parent, 'min_max_in_boolean');
}
}
});
});
});

if (invalidNodes.length === 0) return;

const atRuleParamIndexValue = atRuleParamIndex(atRule);

invalidNodes.forEach((invalidNode) => {
const [start, end] = cssParserAlgorithms.sourceIndices(invalidNode);

report({
message: messages.rejected,
messageArgs: [invalidNode.toString()],
index: atRuleParamIndexValue + start,
endIndex: atRuleParamIndexValue + end + 1,
node: atRule,
ruleName,
result,
});
/**
* @param {import('postcss').AtRule} atRule
* @param {number} index
* @param {{tokens(): Array<import('@csstools/css-tokenizer').CSSToken>}} node
* @param {keyof reasons} [reason]
*/
function complain(atRule, index, node, reason) {
const [start, end] = cssParserAlgorithms.sourceIndices(node);

report({
message: messages.rejected,
messageArgs: [node.toString(), reason ? reasons[reason] : ''],
index: index + start,
endIndex: index + end + 1,
node: atRule,
ruleName,
result,
});
});
}
};
};

Expand Down
70 changes: 41 additions & 29 deletions lib/rules/media-query-no-invalid/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,20 @@ import validateOptions from '../../utils/validateOptions.mjs';
const ruleName = 'media-query-no-invalid';

const messages = ruleMessages(ruleName, {
rejected: (query) => `Unexpected invalid media query "${query}"`,
rejected: (query, reason) => {
if (!reason) return `Unexpected invalid media query "${query}"`;

return `Unexpected invalid media query "${query}", ${reason}`;
},
});

const reasons = {
custom: 'custom media queries can only be used in boolean queries',
min_max_in_range: '"min-" and "max-" prefixes are not needed when using range queries',
min_max_in_boolean: '"min-" and "max-" prefixes are not needed in boolean queries',
discrete: 'discrete features can only be used in plain and boolean queries',
};

const HAS_MIN_MAX_PREFIX = /^(?:min|max)-/i;

const meta = {
Expand All @@ -37,21 +48,20 @@ const rule = (primary) => {
}

root.walkAtRules(/^media$/i, (atRule) => {
/** @type {Array<{tokens(): Array<import('@csstools/css-tokenizer').CSSToken>}>} */
let invalidNodes = [];
const atRuleParamIndexValue = atRuleParamIndex(atRule);

parseMediaQuery(atRule).forEach((mediaQuery) => {
if (isMediaQueryInvalid(mediaQuery)) {
// Queries that fail to parse are invalid.
invalidNodes.push(mediaQuery);
complain(atRule, atRuleParamIndexValue, mediaQuery);

return;
}

mediaQuery.walk(({ node, parent }) => {
// All general enclosed nodes are invalid.
if (isGeneralEnclosed(node)) {
invalidNodes.push(node);
complain(atRule, atRuleParamIndexValue, node);

return;
}
Expand All @@ -62,7 +72,7 @@ const rule = (primary) => {

if (isCustomMediaQuery(name)) {
// In a plain context, custom media queries are invalid.
invalidNodes.push(parent);
complain(atRule, atRuleParamIndexValue, parent, 'custom');

return;
}
Expand All @@ -76,21 +86,21 @@ const rule = (primary) => {

if (isCustomMediaQuery(name)) {
// In a range context, custom media queries are invalid.
invalidNodes.push(parent);
complain(atRule, atRuleParamIndexValue, parent, 'custom');

return;
}

if (HAS_MIN_MAX_PREFIX.test(name)) {
// In a range context, min- and max- prefixed feature names are invalid.
invalidNodes.push(parent);
complain(atRule, atRuleParamIndexValue, parent, 'min_max_in_range');

return;
}

if (!rangeTypeMediaFeatureNames.has(name)) {
// In a range context, non-range typed features are invalid.
invalidNodes.push(parent);
complain(atRule, atRuleParamIndexValue, parent, 'discrete');

return;
}
Expand All @@ -100,34 +110,36 @@ const rule = (primary) => {

// Invalid boolean media features.
if (isMediaFeatureBoolean(node)) {
const name = node.getName().toLowerCase();
const name = node.getName();

if (HAS_MIN_MAX_PREFIX.test(name)) {
// In a range context, min- and max- prefixed feature names are invalid
invalidNodes.push(parent);
// In a boolean feature, min- and max- prefixed feature names are invalid
complain(atRule, atRuleParamIndexValue, parent, 'min_max_in_boolean');
}
}
});
});
});

if (invalidNodes.length === 0) return;

const atRuleParamIndexValue = atRuleParamIndex(atRule);

invalidNodes.forEach((invalidNode) => {
const [start, end] = sourceIndices(invalidNode);

report({
message: messages.rejected,
messageArgs: [invalidNode.toString()],
index: atRuleParamIndexValue + start,
endIndex: atRuleParamIndexValue + end + 1,
node: atRule,
ruleName,
result,
});
/**
* @param {import('postcss').AtRule} atRule
* @param {number} index
* @param {{tokens(): Array<import('@csstools/css-tokenizer').CSSToken>}} node
* @param {keyof reasons} [reason]
*/
function complain(atRule, index, node, reason) {
const [start, end] = sourceIndices(node);

report({
message: messages.rejected,
messageArgs: [node.toString(), reason ? reasons[reason] : ''],
index: index + start,
endIndex: index + end + 1,
node: atRule,
ruleName,
result,
});
});
}
};
};

Expand Down

0 comments on commit 5bb517b

Please sign in to comment.