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 #6963

Merged
merged 10 commits into from Jun 25, 2023
5 changes: 5 additions & 0 deletions .changeset/early-jobs-hunt.md
@@ -0,0 +1,5 @@
---
"stylelint": minor
---

Added: `media-query-no-invalid`
1 change: 1 addition & 0 deletions docs/user-guide/rules.md
Expand Up @@ -58,6 +58,7 @@ Disallow invalid syntax with these (sometimes implicit) `no-invalid` rules.
| [`color-no-invalid-hex`](../../lib/rules/color-no-invalid-hex/README.md)<br/>Disallow invalid hex colors. | ✅ | |
| [`function-calc-no-unspaced-operator`](../../lib/rules/function-calc-no-unspaced-operator/README.md)<br/>Disallow invalid unspaced operator within `calc` functions. | ✅ | 🔧 |
| [`keyframe-declaration-no-important`](../../lib/rules/keyframe-declaration-no-important/README.md)<br/>Disallow invalid `!important` within keyframe declarations. | ✅ | |
| [`media-query-no-invalid`](../../lib/rules/media-query-no-invalid/README.md)<br/>Disallow invalid media queries. | | |
| [`named-grid-areas-no-invalid`](../../lib/rules/named-grid-areas-no-invalid/README.md)<br/>Disallow invalid named grid areas. | ✅ | |
| [`no-invalid-double-slash-comments`](../../lib/rules/no-invalid-double-slash-comments/README.md)<br/>Disallow invalid double-slash comments. | ✅ | |
| [`no-invalid-position-at-import-rule`](../../lib/rules/no-invalid-position-at-import-rule/README.md)<br/>Disallow invalid position `@import` rules. | ✅ | |
Expand Down
1 change: 1 addition & 0 deletions lib/rules/index.js
Expand Up @@ -225,6 +225,7 @@ const rules = {
'media-query-list-comma-space-before': importLazy(() =>
require('./media-query-list-comma-space-before'),
)(),
'media-query-no-invalid': importLazy(() => require('./media-query-no-invalid'))(),
'named-grid-areas-no-invalid': importLazy(() => require('./named-grid-areas-no-invalid'))(),
'no-descending-specificity': importLazy(() => require('./no-descending-specificity'))(),
'no-duplicate-at-import-rules': importLazy(() => require('./no-duplicate-at-import-rules'))(),
Expand Down
11 changes: 11 additions & 0 deletions lib/rules/media-feature-name-no-unknown/__tests__/index.js
Expand Up @@ -62,6 +62,9 @@ testRule({
{
code: '@media () { }',
},
{
code: '@media (grid: 1) {}',
},
romainmenke marked this conversation as resolved.
Show resolved Hide resolved
],

reject: [
Expand Down Expand Up @@ -153,6 +156,14 @@ testRule({
endLine: 1,
endColumn: 49,
},
{
code: '@media (min-grid: 1) { }',
message: messages.rejected('min-grid'),
line: 1,
column: 9,
endLine: 1,
endColumn: 17,
},
],
});

Expand Down
3 changes: 3 additions & 0 deletions lib/rules/media-feature-name-value-no-unknown/index.js
Expand Up @@ -6,6 +6,7 @@ const {
isMediaFeature,
isMediaFeatureValue,
matchesRatioExactly,
isMediaQueryInvalid,
} = require('@csstools/media-query-list-parser');

const atRuleParamIndex = require('../../utils/atRuleParamIndex');
Expand Down Expand Up @@ -239,6 +240,8 @@ const rule = (primary) => {
};

parseMediaQuery(atRule).forEach((mediaQuery) => {
if (isMediaQueryInvalid(mediaQuery)) return;
romainmenke marked this conversation as resolved.
Show resolved Hide resolved

mediaQuery.walk((entry) => {
if (!entry.state) return;

Expand Down
64 changes: 64 additions & 0 deletions lib/rules/media-query-no-invalid/README.md
@@ -0,0 +1,64 @@
# media-query-no-invalid

Disallow invalid media queries.

<!-- prettier-ignore -->
```css
@media not(min-width: 300px) {}
/** ↑
* This media query */
```

Media queries must be grammatically valid according to the [Media Queries Level 5](https://www.w3.org/TR/mediaqueries-5/) specification.
Mouvedia marked this conversation as resolved.
Show resolved Hide resolved

This rule is only appropriate for CSS. You should not turn it on for CSS-like languages, such as Sass or Less, as they have their own syntaxes.

It works well with other rules that validate feature names and values:

- [`media-feature-name-no-unknown`](../media-feature-name-no-unknown/README.md)
- [`media-feature-name-value-no-unknown`](../media-feature-name-value-no-unknown/README.md)

The [`message` secondary option](../../../docs/user-guide/configure.md#message) can accept the arguments of this rule.

## Options

### `true`

The following patterns are considered problems:

<!-- prettier-ignore -->
```css
@media not(min-width: 300px) {}
```

<!-- prettier-ignore -->
```css
@media (width == 100px) {}
```

<!-- prettier-ignore -->
```css
@media (color) and (hover) or (width) {}
```

The following patterns are _not_ considered problems:

<!-- prettier-ignore -->
```css
@media not (min-width: 300px) {}
```

<!-- prettier-ignore -->
```css
@media (width = 100px) {}
```

<!-- prettier-ignore -->
```css
@media ((color) and (hover)) or (width) {}
```

<!-- prettier-ignore -->
```css
@media (color) and ((hover) or (width)) {}
```
267 changes: 267 additions & 0 deletions lib/rules/media-query-no-invalid/__tests__/index.js
@@ -0,0 +1,267 @@
'use strict';

const { messages, ruleName } = require('..');

testRule({
ruleName,
config: [true],

accept: [
{
code: '@media screen {}',
},
{
code: '@media print {}',
},
{
code: '@media all {}',
},
{
code: '@media {}',
},
{
code: '@media screen and (color) {}',
},
{
code: '@media only screen and (color) {}',
},
{
code: '@media speech and (device-aspect-ratio: 16/9) {}',
},
{
code: '@media (width >= 600px) {}',
},
{
code: '@media (width: 600px) {}',
},
{
code: '@media not (width <= -100px) {}',
},
{
code: '@media not (resolution: -300dpi) {}',
},
{
code: '@media (min-width: 320.01px) {}',
},
{
code: '@media not (color) {}',
},
{
code: '@media (width < 600px) and (height < 600px) {}',
},
{
code: '@media (width < 600px) and (height < 600px) {}',
},
{
code: '@media (not (color)) or (hover) {}',
},
{
code: '@media (not (color)) and (not (hover)) {}',
},
{
code: '@media (not (color)) and (not (hover)) {}',
},
{
code: '@media screen and (max-weight: 3kg) and (color), (color) {}',
},
{
code: '@media not unknown {}',
},
],

reject: [
{
code: '@media @foo {}',
message: messages.rejected('@foo'),
line: 1,
column: 8,
endLine: 1,
endColumn: 12,
},
{
code: '@media screen or (min-width > 500px) {}',
message: messages.rejected('screen or (min-width > 500px)'),
line: 1,
column: 8,
endLine: 1,
endColumn: 37,
},
{
code: '@media ((min-width: 300px) and (hover: hover) or (aspect-ratio: 1 / 1)) {}',
message: messages.rejected(
'((min-width: 300px) and (hover: hover) or (aspect-ratio: 1 / 1))',
),
line: 1,
column: 8,
endLine: 1,
endColumn: 72,
},
{
code: '@media (min-width: var(--foo)) {}',
message: messages.rejected('(min-width: var(--foo))'),
line: 1,
column: 8,
endLine: 1,
endColumn: 31,
},
{
code: '@media (min-width: 50%) {}',
message: messages.rejected('(min-width: 50%)'),
line: 1,
column: 8,
endLine: 1,
endColumn: 24,
},
{
code: '@media ((color: 2) and (min-width: 50%)) {}',
message: messages.rejected('(min-width: 50%)'),
line: 1,
column: 24,
endLine: 1,
endColumn: 40,
},
{
code: '@media ((color: foo(--bar)) and (min-width: 50%)) {}',
warnings: [
{
message: messages.rejected('(color: foo(--bar))'),
line: 1,
column: 9,
endLine: 1,
endColumn: 28,
},
{
message: messages.rejected('(min-width: 50%)'),
line: 1,
column: 33,
endLine: 1,
endColumn: 49,
},
],
},
{
code: '@media (color: foo(--bar)), (min-width: 50%) {}',
warnings: [
{
message: messages.rejected('(color: foo(--bar))'),
line: 1,
column: 8,
endLine: 1,
endColumn: 27,
},
{
message: messages.rejected('(min-width: 50%)'),
line: 1,
column: 29,
endLine: 1,
endColumn: 45,
},
],
},
{
code: '@media (--foo: 2) {}',
message: messages.rejected('(--foo: 2)'),
line: 1,
column: 8,
endLine: 1,
endColumn: 18,
},
{
code: '@media (min-width < 500px) {}',
message: messages.rejected('(min-width < 500px)'),
line: 1,
column: 8,
endLine: 1,
endColumn: 27,
},
{
code: '@media (min-width) {}',
message: messages.rejected('(min-width)'),
line: 1,
column: 8,
endLine: 1,
endColumn: 19,
},
{
code: '@media (grid < 0) {}',
message: messages.rejected('(grid < 0)'),
line: 1,
column: 8,
endLine: 1,
endColumn: 18,
},
{
code: '@media (not (color) and (hover)) {}',
message: messages.rejected('(not (color) and (hover))'),
line: 1,
column: 8,
endLine: 1,
endColumn: 33,
},

{
code: '@media (color) and (hover) or (width) {}',
message: messages.rejected('(color) and (hover) or (width)'),
line: 1,
column: 8,
endLine: 1,
endColumn: 38,
},
{
code: '@media (width => 100px) {}',
message: messages.rejected('(width => 100px)'),
line: 1,
column: 8,
endLine: 1,
endColumn: 24,
},
{
code: '@media (width == 100px) {}',
message: messages.rejected('(width == 100px)'),
line: 1,
column: 8,
endLine: 1,
endColumn: 24,
},
{
code: '@media not(min-width: 100px) {}',
message: messages.rejected('not(min-width: 100px)'),
line: 1,
column: 8,
endLine: 1,
endColumn: 29,
},
{
code: '@media foo screen {}',
message: messages.rejected('foo screen'),
line: 1,
column: 8,
endLine: 1,
endColumn: 18,
},
{
code: '@media not screen foo (min-width: 300px) {}',
message: messages.rejected('not screen foo (min-width: 300px)'),
line: 1,
column: 8,
endLine: 1,
endColumn: 41,
},
{
code: '@media (--foo: 300px) {}',
message: messages.rejected('(--foo: 300px)'),
line: 1,
column: 8,
endLine: 1,
endColumn: 22,
},
{
code: '@media (--foo < 300px) {}',
message: messages.rejected('(--foo < 300px)'),
line: 1,
column: 8,
endLine: 1,
endColumn: 23,
},
],
});