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

Deprecated mixed declarations #2267

Merged
merged 6 commits into from
Jul 9, 2024
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
Loading