Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
1 change: 1 addition & 0 deletions lib/src/ast/sass.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export 'sass/statement/include_rule.dart';
export 'sass/statement/loud_comment.dart';
export 'sass/statement/media_rule.dart';
export 'sass/statement/mixin_rule.dart';
export 'sass/statement/nest_rule.dart';
export 'sass/statement/parent.dart';
export 'sass/statement/return_rule.dart';
export 'sass/statement/silent_comment.dart';
Expand Down
28 changes: 28 additions & 0 deletions lib/src/ast/sass/statement/nest_rule.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright 2024 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

import 'package:source_span/source_span.dart';

import '../../../visitor/interface/statement.dart';
import '../statement.dart';
import 'parent.dart';

/// A `@nest` rule.
///
/// This ensures that the nesting and ordering of its contents match that
/// [specified by CSS].
///
/// [specified by CSS]: https://drafts.csswg.org/css-nesting/#mixing
///
/// {@category AST}
final class NestRule extends ParentStatement<List<Statement>> {
final FileSpan span;

NestRule(Iterable<Statement> children, this.span)
: super(List.unmodifiable(children));

T accept<T>(StatementVisitor<T> visitor) => visitor.visitNestRule(this);

String toString() => "@nest {${children.join(' ')}}";
}
7 changes: 6 additions & 1 deletion lib/src/deprecation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ enum Deprecation {
// DO NOT EDIT. This section was generated from the language repo.
// See tool/grind/generate_deprecations.dart for details.
//
// Checksum: 22d9bdbe92eb39b3c0d6d64ebe1879a431c0037e
// Checksum: 309e4f1f008f08379b824ab6094e13df2e18e187

/// Deprecation for passing a string directly to meta.call().
callString('call-string',
Expand Down Expand Up @@ -90,6 +90,11 @@ enum Deprecation {
deprecatedIn: '1.76.0',
description: 'Function and mixin names beginning with --.'),

/// Deprecation for declarations after or between nested rules.
mixedDecls('mixed-decls',
deprecatedIn: '1.77.7',
description: 'Declarations after or between nested rules.'),

/// Deprecation for @import rules.
import.future('import', description: '@import rules.'),

Expand Down
7 changes: 4 additions & 3 deletions lib/src/parse/css.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class CssParser extends ScssParser {
}

Statement atRule(Statement child(), {bool root = false}) {
// NOTE: this logic is largely duplicated in CssParser.atRule. Most changes
// NOTE: this logic is largely duplicated in StylesheetParser.atRule. Most changes
// here should be mirrored there.

var start = scanner.state;
Expand All @@ -65,17 +65,18 @@ class CssParser extends ScssParser {
"return" ||
"warn" ||
"while" =>
_forbiddenAtRoot(start),
_forbiddenAtRule(start),
"import" => _cssImportRule(start),
"media" => mediaRule(start),
"-moz-document" => mozDocumentRule(start, name),
"supports" => supportsRule(start),
var name? when name.toLowerCase() == "nest" => nestRule(start),
_ => unknownAtRule(start, name)
};
}

/// Throws an error for a forbidden at-rule.
Never _forbiddenAtRoot(LineScannerState start) {
Never _forbiddenAtRule(LineScannerState start) {
almostAnyValue();
error("This at-rule isn't allowed in plain CSS.", scanner.spanFrom(start));
}
Expand Down
18 changes: 18 additions & 0 deletions lib/src/parse/stylesheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,8 @@ abstract class StylesheetParser extends Parser {
return _warnRule(start);
case "while":
return _whileRule(start, child);
case var name? when name.toLowerCase() == "nest":
return nestRule(start);
default:
return unknownAtRule(start, name);
}
Expand Down Expand Up @@ -1400,6 +1402,22 @@ abstract class StylesheetParser extends Parser {
});
}

/// Consumes a `@return` rule.
///
/// [start] should point before the `@`.
@protected
NestRule nestRule(LineScannerState start) {
whitespace();
var wasInUnknownAtRule = _inUnknownAtRule;
_inUnknownAtRule = true;
try {
return _withChildren(
_statement, start, (children, span) => NestRule(children, span));
} finally {
_inUnknownAtRule = wasInUnknownAtRule;
}
}

/// Consumes a `@return` rule.
///
/// [start] should point before the `@`.
Expand Down
82 changes: 74 additions & 8 deletions lib/src/visitor/async_evaluate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1189,6 +1189,20 @@ final class _EvaluateVisitor
node.span);
}

if (_parent.parent!.children.last case var sibling
when _parent != sibling) {
_warn(
"Sass's behavior for declarations that appear after nested\n"
"rules will be changing to match the behavior specified by CSS in an "
"upcoming\n"
"version. To keep the existing behavior, move the declaration above "
"the nested\n"
"rule. To opt into the new behavior, wrap the declaration in `@nest "
"{}`.",
MultiSpan(node.span, 'declaration', {sibling.span: 'nested rule'}),
Deprecation.mixedDecls);
}

var name = await _interpolationToValue(node.name, warnForColor: true);
if (_declarationName case var declarationName?) {
name = CssValue("$declarationName-${name.value}", name.span);
Expand Down Expand Up @@ -1884,6 +1898,53 @@ final class _EvaluateVisitor
return null;
}

Future<Value?> visitNestRule(NestRule node) async {
if (_declarationName != null) {
throw _exception(
"At-rules may not be used within nested declarations.", node.span);
} else if (_inKeyframes) {
throw _exception(
"@nest may not be used within a keyframe block.", node.span);
}

var wasInUnknownAtRule = _inUnknownAtRule;
var oldAtRootExcludingStyleRule = _atRootExcludingStyleRule;
_inUnknownAtRule = true;
_atRootExcludingStyleRule = false;
if (_styleRule case var styleRule?) {
if (_stylesheet.plainCss) {
for (var child in node.children) {
await child.accept(this);
}
} else {
var newStyleRule = styleRule.copyWithoutChildren();
await _withParent(newStyleRule, () async {
await _withStyleRule(newStyleRule, () async {
for (var child in node.children) {
await child.accept(this);
}
});
},
through: (node) => node is CssStyleRule,
scopeWhen: node.hasDeclarations);

_warnForBogusCombinators(newStyleRule);
}
} else {
await _withParent(
ModifiableCssAtRule(CssValue("nest", node.span), node.span),
() async {
for (var child in node.children) {
await child.accept(this);
}
}, scopeWhen: node.hasDeclarations);
}
_inUnknownAtRule = wasInUnknownAtRule;
_atRootExcludingStyleRule = oldAtRootExcludingStyleRule;

return null;
}

Future<Value?> visitLoudComment(LoudComment node) async {
// NOTE: this logic is largely duplicated in [visitCssComment]. Most changes
// here should be mirrored there.
Expand Down Expand Up @@ -2065,8 +2126,20 @@ final class _EvaluateVisitor
scopeWhen: node.hasDeclarations);
_atRootExcludingStyleRule = oldAtRootExcludingStyleRule;

_warnForBogusCombinators(rule);

if (_styleRule == null && _parent.children.isNotEmpty) {
var lastChild = _parent.children.last;
lastChild.isGroupEnd = true;
}

return null;
}

/// Emits deprecation warnings for any bogus combinators in [rule].
void _warnForBogusCombinators(CssStyleRule rule) {
if (!rule.isInvisibleOtherThanBogusCombinators) {
for (var complex in parsedSelector.components) {
for (var complex in rule.selector.components) {
if (!complex.isBogus) continue;

if (complex.isUseless) {
Expand Down Expand Up @@ -2110,13 +2183,6 @@ final class _EvaluateVisitor
}
}
}

if (_styleRule == null && _parent.children.isNotEmpty) {
var lastChild = _parent.children.last;
lastChild.isGroupEnd = true;
}

return null;
}

Future<Value?> visitSupportsRule(SupportsRule node) async {
Expand Down
83 changes: 74 additions & 9 deletions lib/src/visitor/evaluate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// DO NOT EDIT. This file was generated from async_evaluate.dart.
// See tool/grind/synchronize.dart for details.
//
// Checksum: 116b8079719577ac6e4dad4aebe403282136e611
// Checksum: e1e8310eb9afa8042569f8a706341ae983fd78ee
//
// ignore_for_file: unused_import

Expand Down Expand Up @@ -1187,6 +1187,20 @@ final class _EvaluateVisitor
node.span);
}

if (_parent.parent!.children.last case var sibling
when _parent != sibling) {
_warn(
"Sass's behavior for declarations that appear after nested\n"
"rules will be changing to match the behavior specified by CSS in an "
"upcoming\n"
"version. To keep the existing behavior, move the declaration above "
"the nested\n"
"rule. To opt into the new behavior, wrap the declaration in `@nest "
"{}`.",
MultiSpan(node.span, 'declaration', {sibling.span: 'nested rule'}),
Deprecation.mixedDecls);
}

var name = _interpolationToValue(node.name, warnForColor: true);
if (_declarationName case var declarationName?) {
name = CssValue("$declarationName-${name.value}", name.span);
Expand Down Expand Up @@ -1876,6 +1890,52 @@ final class _EvaluateVisitor
return null;
}

Value? visitNestRule(NestRule node) {
if (_declarationName != null) {
throw _exception(
"At-rules may not be used within nested declarations.", node.span);
} else if (_inKeyframes) {
throw _exception(
"@nest may not be used within a keyframe block.", node.span);
}

var wasInUnknownAtRule = _inUnknownAtRule;
var oldAtRootExcludingStyleRule = _atRootExcludingStyleRule;
_inUnknownAtRule = true;
_atRootExcludingStyleRule = false;
if (_styleRule case var styleRule?) {
if (_stylesheet.plainCss) {
for (var child in node.children) {
child.accept(this);
}
} else {
var newStyleRule = styleRule.copyWithoutChildren();
_withParent(newStyleRule, () {
_withStyleRule(newStyleRule, () {
for (var child in node.children) {
child.accept(this);
}
});
},
through: (node) => node is CssStyleRule,
scopeWhen: node.hasDeclarations);

_warnForBogusCombinators(newStyleRule);
}
} else {
_withParent(ModifiableCssAtRule(CssValue("nest", node.span), node.span),
() {
for (var child in node.children) {
child.accept(this);
}
}, scopeWhen: node.hasDeclarations);
}
_inUnknownAtRule = wasInUnknownAtRule;
_atRootExcludingStyleRule = oldAtRootExcludingStyleRule;

return null;
}

Value? visitLoudComment(LoudComment node) {
// NOTE: this logic is largely duplicated in [visitCssComment]. Most changes
// here should be mirrored there.
Expand Down Expand Up @@ -2055,8 +2115,20 @@ final class _EvaluateVisitor
scopeWhen: node.hasDeclarations);
_atRootExcludingStyleRule = oldAtRootExcludingStyleRule;

_warnForBogusCombinators(rule);

if (_styleRule == null && _parent.children.isNotEmpty) {
var lastChild = _parent.children.last;
lastChild.isGroupEnd = true;
}

return null;
}

/// Emits deprecation warnings for any bogus combinators in [rule].
void _warnForBogusCombinators(CssStyleRule rule) {
if (!rule.isInvisibleOtherThanBogusCombinators) {
for (var complex in parsedSelector.components) {
for (var complex in rule.selector.components) {
if (!complex.isBogus) continue;

if (complex.isUseless) {
Expand Down Expand Up @@ -2100,13 +2172,6 @@ final class _EvaluateVisitor
}
}
}

if (_styleRule == null && _parent.children.isNotEmpty) {
var lastChild = _parent.children.last;
lastChild.isGroupEnd = true;
}

return null;
}

Value? visitSupportsRule(SupportsRule node) {
Expand Down
1 change: 1 addition & 0 deletions lib/src/visitor/interface/statement.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ abstract interface class StatementVisitor<T> {
T visitLoudComment(LoudComment node);
T visitMediaRule(MediaRule node);
T visitMixinRule(MixinRule node);
T visitNestRule(NestRule node);
T visitReturnRule(ReturnRule node);
T visitSilentComment(SilentComment node);
T visitStyleRule(StyleRule node);
Expand Down
2 changes: 2 additions & 0 deletions lib/src/visitor/recursive_statement.dart
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ mixin RecursiveStatementVisitor implements StatementVisitor<void> {

void visitMixinRule(MixinRule node) => visitCallableDeclaration(node);

void visitNestRule(NestRule node) => visitChildren(node.children);

void visitReturnRule(ReturnRule node) {}

void visitSilentComment(SilentComment node) {}
Expand Down
2 changes: 2 additions & 0 deletions lib/src/visitor/statement_search.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ mixin StatementSearchVisitor<T> implements StatementVisitor<T?> {

T? visitMixinRule(MixinRule node) => visitCallableDeclaration(node);

T? visitNestRule(NestRule node) => visitChildren(node.children);

T? visitReturnRule(ReturnRule node) => null;

T? visitSilentComment(SilentComment node) => null;
Expand Down