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

Add media-query-no-invalid specific problem messages #7462

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/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();
Mouvedia marked this conversation as resolved.
Show resolved Hide resolved

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