Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e649efa
add `declaration-block-nesting-at-rule-required-list`
sw1tch3roo Jul 22, 2025
d51f557
Merge branch 'main' into feature/declaration-block-nesting-at-rule-re…
sw1tch3roo Jul 22, 2025
3c04e94
fix the type in index.d.ts
sw1tch3roo Jul 22, 2025
3fe8df7
Refactor rule to check entire rules instead of individual declarations
sw1tch3roo Jul 23, 2025
016b16e
Merge branch 'main' into feature/declaration-block-nesting-at-rule-re…
sw1tch3roo Jul 24, 2025
cbade06
changes after review: change code style, delete unnecessary tests, up…
sw1tch3roo Jul 24, 2025
52c7654
Merge branch 'main' into feature/declaration-block-nesting-at-rule-re…
sw1tch3roo Jul 24, 2025
e048d14
suggestions from code review
sw1tch3roo Jul 24, 2025
18b7ec7
Merge branch 'main' into feature/declaration-block-nesting-at-rule-re…
sw1tch3roo Jul 25, 2025
2bdb301
build cjs file
sw1tch3roo Jul 25, 2025
db6523f
use typeGuards utils
sw1tch3roo Jul 25, 2025
892693a
Merge branch 'main' into feature/declaration-block-nesting-at-rule-re…
sw1tch3roo Jul 25, 2025
d90f473
enhance the expected messages
sw1tch3roo Jul 28, 2025
fb24ded
Merge branch 'main' into feature/declaration-block-nesting-at-rule-re…
sw1tch3roo Jul 28, 2025
e79ff39
change the examples in README.md
sw1tch3roo Jul 28, 2025
9f765c3
Merge branch 'main' into feature/declaration-block-nesting-at-rule-re…
sw1tch3roo Jul 29, 2025
fa64d61
Merge branch 'main' into feature/declaration-block-nesting-at-rule-re…
sw1tch3roo Jul 30, 2025
dd2a7ff
fix expected message type in `d.ts` declaration
sw1tch3roo Aug 2, 2025
554ecd6
reuse the type of args
sw1tch3roo Aug 2, 2025
5dca318
change expected message type in `d.ts`
sw1tch3roo Aug 2, 2025
c0c5591
Merge branch 'main' into feature/declaration-block-nesting-at-rule-re…
sw1tch3roo Aug 4, 2025
f4000e8
Merge branch 'main' into feature/declaration-block-nesting-at-rule-re…
sw1tch3roo Aug 5, 2025
6260d46
Merge branch 'main' into feature/declaration-block-nesting-at-rule-re…
sw1tch3roo Aug 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
add declaration-block-nesting-at-rule-required-list
  • Loading branch information
sw1tch3roo committed Jul 22, 2025
commit e649efa69e99b5a3084d4ee6c1e0ab2d395911a7
5 changes: 5 additions & 0 deletions .changeset/great-geese-wink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"stylelint": minor
---

Added: `declaration-block-nesting-at-rule-required-list` rule
1 change: 1 addition & 0 deletions docs/user-guide/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ Allow, disallow or require things with these `allowed-list`, `disallowed-list`,
<!-- prettier-ignore-start -->
| | | |
| :-- | :-: | :-: |
| [`declaration-block-nesting-at-rule-required-list`](../../lib/rules/declaration-block-nesting-at-rule-required-list/README.md)<br/>Require specific at-rules to be present around CSS declarations. | | |
| [`declaration-no-important`](../../lib/rules/declaration-no-important/README.md)<br/>Disallow `!important` within declarations. | | |
| [`declaration-property-unit-allowed-list`](../../lib/rules/declaration-property-unit-allowed-list/README.md)<br/>Specify a list of allowed property and unit pairs within declarations. | | |
| [`declaration-property-unit-disallowed-list`](../../lib/rules/declaration-property-unit-disallowed-list/README.md)<br/>Specify a list of disallowed property and unit pairs within declarations. | | |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# declaration-block-nesting-at-rule-required-list

Require specific at-rules to be present around CSS declarations.

<!-- prettier-ignore -->
```css
a { color: red; }
/** ↑
* This declaration */
```

## Options

`array|string|regex`: `["layer"]`

Given:

```json
["layer"]
```

The following patterns are considered problems:

<!-- prettier-ignore -->
```css
a { color: red; }
```

<!-- prettier-ignore -->
```css
@media all {
a { color: red; }
}
```

The following patterns are _not_ considered problems:

<!-- prettier-ignore -->
```css
@layer {
a { color: red; }
}
```

<!-- prettier-ignore -->
```css
a {
@layer {
color: red;
}
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import rule from '../index.mjs';
const { messages, ruleName } = rule;

testRule({
ruleName,
config: ['layer', '/^s/'],

accept: [
{
code: '@layer { a { color: red; } }',
description: 'declaration within @layer at-rule',
},
{
code: 'a { @layer { color: red; } }',
description: 'declaration within nested @layer at-rule',
},
{
code: '@supports (color: red) { a { color: red; } }',
description: 'declaration within @supports at-rule (matches regex)',
},
{
code: '@scope (a) { a { color: red; } }',
description: 'declaration within @scope at-rule (matches regex)',
},
{
code: '@media all {}',
description: 'empty at-rule',
},
{
code: '@layer { @media all { a { color: red; } } }',
description: 'declaration within nested required at-rule',
},
],

reject: [
{
code: 'a { color: red; }',
description: 'declaration without required at-rule',
message: messages.expected('layer, /^s/'),
line: 1,
column: 5,
endLine: 1,
endColumn: 16,
},
{
code: 'a { color: red; color: blue; }',
description: 'multiple declarations without required at-rule',
warnings: [
{
message: messages.expected('layer, /^s/'),
line: 1,
column: 5,
endLine: 1,
endColumn: 16,
},
{
message: messages.expected('layer, /^s/'),
line: 1,
column: 17,
endLine: 1,
endColumn: 29,
},
],
},
{
code: '@media all { a { color: red; } }',
description: 'declaration within non-required at-rule',
message: messages.expected('layer, /^s/'),
line: 1,
column: 18,
endLine: 1,
endColumn: 29,
},
{
code: 'a { @media all { color: red; } }',
description: 'declaration within nested non-required at-rule',
message: messages.expected('layer, /^s/'),
line: 1,
column: 18,
endLine: 1,
endColumn: 29,
},
],
});

testRule({
ruleName,
config: ['layer'],

accept: [
{
code: '@layer { a { color: red; } }',
description: 'declaration within @layer at-rule',
},
{
code: 'a { @layer { color: red; } }',
description: 'declaration within nested @layer at-rule',
},
],

reject: [
{
code: 'a { color: red; }',
description: 'declaration without required at-rule',
message: messages.expected('layer'),
line: 1,
column: 5,
endLine: 1,
endColumn: 16,
},
{
code: '@supports (color: red) { a { color: red; } }',
description: 'declaration within non-required at-rule',
message: messages.expected('layer'),
line: 1,
column: 30,
endLine: 1,
endColumn: 41,
},
],
});

testRule({
ruleName,
config: ['/^s/'],

accept: [
{
code: '@supports (color: red) { a { color: red; } }',
description: 'declaration within @supports at-rule (matches regex)',
},
{
code: '@scope (a) { a { color: red; } }',
description: 'declaration within @scope at-rule (matches regex)',
},
],

reject: [
{
code: 'a { color: red; }',
description: 'declaration without required at-rule',
message: messages.expected('/^s/'),
line: 1,
column: 5,
endLine: 1,
endColumn: 16,
},
{
code: '@layer { a { color: red; } }',
description: 'declaration within non-matching at-rule',
message: messages.expected('/^s/'),
line: 1,
column: 14,
endLine: 1,
endColumn: 25,
},
],
});

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { isRegExp, isString } from '../../utils/validateTypes.mjs';
import eachDeclarationBlock from '../../utils/eachDeclarationBlock.mjs';
import isStandardSyntaxDeclaration from '../../utils/isStandardSyntaxDeclaration.mjs';
import matchesStringOrRegExp from '../../utils/matchesStringOrRegExp.mjs';
import report from '../../utils/report.mjs';
import ruleMessages from '../../utils/ruleMessages.mjs';
import validateOptions from '../../utils/validateOptions.mjs';

const ruleName = 'declaration-block-nesting-at-rule-required-list';

const messages = ruleMessages(ruleName, {
expected: (atRule) => `Expected nesting at-rule "${atRule}" for declaration`,
});

const meta = {
url: 'https://stylelint.io/user-guide/rules/declaration-block-nesting-at-rule-required-list',
};

/** @type {import('stylelint').CoreRules[ruleName]} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: [isString, isRegExp],
});

if (!validOptions) return;

eachDeclarationBlock(root, (eachDecl) => {
eachDecl((decl) => {
if (!isStandardSyntaxDeclaration(decl)) return;

// Skip if declaration already has a required at-rule ancestor
if (hasRequiredAtRuleAncestor(decl, primary)) return;

report({
message: messages.expected,
messageArgs: [Array.isArray(primary) ? primary.join(', ') : primary],
node: decl,
result,
ruleName,
});
});
});
};
};

/**
* @param {import('postcss').Declaration} decl
* @param {string | RegExp | Array<string | RegExp>} requiredAtRules
* @returns {boolean}
*/
function hasRequiredAtRuleAncestor(decl, requiredAtRules) {
let current = decl.parent;

while (current) {
if (current.type === 'atrule') {
const atRuleName = current.name;

// Check if at-rule name matches any of the required patterns
if (matchesStringOrRegExp(atRuleName, requiredAtRules)) {
return true;
}
}

if (current.type === 'root') {
break;
}

current = current.parent;
}

return false;
}

rule.primaryOptionArray = true;
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
export default rule;
Loading