[csslib] Fix StateError on error tokens, polyfill var mutation, and cyclic mixins
- Guard `Token.toString()` in `identifier()`, `simpleSelector()`, and `processVariableOrDirective()` via `_safeTokenString` so `ERROR` and `UNTERMINATED_COMMENT` tokens do not throw `StateError('Unknown TOKEN')`.
- Snapshot `Expressions.expressions` in `PolyFill.processVars` before `_findTerminalVarDefinition` mutates the list via `replaceRange`.
- Prevent infinite node expansion on self-referential and cyclic `@mixin` / `@include` definitions in `TopLevelIncludes` and `DeclarationIncludes`, and guard `include.span is FileSpan` in `processMixin()`.
diff --git a/pkgs/csslib/CHANGELOG.md b/pkgs/csslib/CHANGELOG.md
index 6209079..5b1d280 100644
--- a/pkgs/csslib/CHANGELOG.md
+++ b/pkgs/csslib/CHANGELOG.md
@@ -1,9 +1,10 @@
## 1.0.3-wip
-- Fix unhandled exceptions (`RangeError`, `FormatException`, `TypeError`,
- `StackOverflowError`, and `AssertionError`) and infinite loops across the
- tokenizer, parser, analyzer, polyfill pass, and printers when processing
- malformed or deeply nested CSS, SCSS, and Less inputs.
+- Fix unhandled exceptions (`RangeError`, `FormatException`, `StateError`,
+ `ConcurrentModificationError`, `TypeError`, `StackOverflowError`, and
+ `AssertionError`) and infinite loops across the tokenizer, parser, analyzer,
+ polyfill pass, and printers when processing malformed or deeply nested CSS,
+ SCSS, and Less inputs.
## 1.0.2
diff --git a/pkgs/csslib/lib/parser.dart b/pkgs/csslib/lib/parser.dart
index 082b8f6..36445a8 100644
--- a/pkgs/csslib/lib/parser.dart
+++ b/pkgs/csslib/lib/parser.dart
@@ -308,6 +308,14 @@
_error(message, tok.span);
}
+ String _safeTokenString(Token tok) {
+ try {
+ return tok.toString();
+ } catch (_) {
+ return tok.text;
+ }
+ }
+
void _error(String message, SourceSpan? location) {
location ??= _peekToken.span;
messages.error(message, location);
@@ -806,8 +814,11 @@
if (include is IncludeDirective) {
newDecls.add(IncludeMixinAtDeclaration(include, include.span));
} else {
- _warning('Error mixing of top-level vs declarations mixins',
- _makeSpan(include.span as FileSpan));
+ _warning(
+ 'Error mixing of top-level vs declarations mixins',
+ include.span is FileSpan
+ ? _makeSpan(include.span as FileSpan)
+ : include.span);
}
}
declGroup.declarations.insertAll(0, newDecls);
@@ -890,7 +901,8 @@
var span = _makeSpan(start);
return VarDefinitionDirective(VarDefinition(name, exprs, span), span);
} else if (isChecked) {
- _error('unexpected directive @$_peekToken', _peekToken.span);
+ _error('unexpected directive @${_safeTokenString(_peekToken)}',
+ _peekToken.span);
}
}
} else if (mixinParameter && _peekToken.kind == TokenKind.VAR_DEFINITION) {
@@ -1470,7 +1482,9 @@
element = identifier();
break;
default:
- _error('expected element name or universal(*), but found $_peekToken',
+ _error(
+ 'expected element name or universal(*), '
+ 'but found ${_safeTokenString(_peekToken)}',
_peekToken.span);
element = Identifier('', _makeSpan(start));
break;
@@ -2716,7 +2730,8 @@
if (!TokenKind.isIdentifier(tok.kind) &&
!TokenKind.isKindIdentifier(tok.kind)) {
if (isChecked) {
- _warning('expected identifier, but found $tok', tok.span);
+ _warning('expected identifier, but found ${_safeTokenString(tok)}',
+ tok.span);
}
return Identifier('', _makeSpan(tok.span));
}
diff --git a/pkgs/csslib/lib/src/analyzer.dart b/pkgs/csslib/lib/src/analyzer.dart
index b38ff72..397aebc 100644
--- a/pkgs/csslib/lib/src/analyzer.dart
+++ b/pkgs/csslib/lib/src/analyzer.dart
@@ -482,7 +482,9 @@
@override
void visitIncludeDirective(IncludeDirective node) {
final currDef = this.currDef;
- if (map.containsKey(node.name)) {
+ if (currDef is MixinRulesetDirective && node.name == currDef.name) {
+ currDef.rulesets.removeWhere((entry) => entry == node);
+ } else if (map.containsKey(node.name)) {
var mixinDef = map[node.name];
if (mixinDef is MixinRulesetDirective) {
_TopLevelIncludeReplacer.replace(_styleSheet!, node, mixinDef.rulesets);
@@ -536,6 +538,7 @@
/// @include as a top-level with ruleset(s).
class _TopLevelIncludeReplacer extends Visitor {
+ static const int _maxExpansions = 256;
final IncludeDirective _include;
final List<TreeNode> _newRules;
@@ -553,7 +556,9 @@
void visitStyleSheet(StyleSheet node) {
var index = node.topLevels.indexOf(_include);
if (index != -1) {
- node.topLevels.insertAll(index + 1, _newRules);
+ if (node.topLevels.length < _maxExpansions) {
+ node.topLevels.insertAll(index + 1, _newRules);
+ }
node.topLevels.replaceRange(index, index + 1, [NoOp()]);
}
super.visitStyleSheet(node);
@@ -563,7 +568,9 @@
void visitMixinRulesetDirective(MixinRulesetDirective node) {
var index = node.rulesets.indexOf(_include);
if (index != -1) {
- node.rulesets.insertAll(index + 1, _newRules);
+ if (node.rulesets.length < _maxExpansions) {
+ node.rulesets.insertAll(index + 1, _newRules);
+ }
// Only the resolve the @include once.
node.rulesets.replaceRange(index, index + 1, [NoOp()]);
}
@@ -882,6 +889,7 @@
/// @include as a top-level with ruleset(s).
class _IncludeReplacer extends Visitor {
+ static const int _maxExpansions = 256;
final TreeNode _include;
final List<TreeNode> _newDeclarations;
@@ -899,7 +907,9 @@
void visitDeclarationGroup(DeclarationGroup node) {
var index = _findInclude(node.declarations, _include);
if (index != -1) {
- node.declarations.insertAll(index + 1, _newDeclarations);
+ if (node.declarations.length < _maxExpansions) {
+ node.declarations.insertAll(index + 1, _newDeclarations);
+ }
// Change @include to NoOp so it's processed only once.
node.declarations.replaceRange(index, index + 1, [NoOp()]);
}
diff --git a/pkgs/csslib/lib/src/polyfill.dart b/pkgs/csslib/lib/src/polyfill.dart
index e35dc19..5d49213 100644
--- a/pkgs/csslib/lib/src/polyfill.dart
+++ b/pkgs/csslib/lib/src/polyfill.dart
@@ -47,7 +47,7 @@
// Resolve all definitions to a non-VarUsage (terminal expression).
mainStyleSheetVarDefs.forEach((key, value) {
if (value.expression is Expressions) {
- for (var _ in (value.expression as Expressions).expressions) {
+ for (var _ in (value.expression as Expressions).expressions.toList()) {
mainStyleSheetVarDefs[key] =
_findTerminalVarDefinition(_allVarDefinitions, value);
}
diff --git a/pkgs/csslib/test/error_test.dart b/pkgs/csslib/test/error_test.dart
index b59a817..f5677bd 100644
--- a/pkgs/csslib/test/error_test.dart
+++ b/pkgs/csslib/test/error_test.dart
@@ -431,12 +431,21 @@
_exerciseAllEntryPoints('@: 1px; a { var-: 2px; }');
_exerciseAllEntryPoints(
'@mixin m(@a) { color: @a; } @include ; a { @include m(); }');
+ _exerciseAllEntryPoints(
+ '@mixin m { @media screen { } color: red; } @include m;');
+ _exerciseAllEntryPoints(
+ '@mixin a { color: red; } @mixin a { @include a; } '
+ '@include a; div { @include a; }');
+ _exerciseAllEntryPoints(
+ '@mixin b { color: red; } @mixin a { @include b; } '
+ '@mixin b { @include a; } div { @include b; }');
});
test('malformed @-moz-document and @supports directives', () {
_exerciseAllEntryPoints(
'@-moz-document regexp(".*"), foo(1), url-prefix() '
'{ a { top: 0; } }');
+ _exerciseAllEntryPoints('@-moz-document\uFFFDfoo(1) { a { top: 0; } }');
_exerciseAllEntryPoints('@-moz-document { }');
_exerciseAllEntryPoints('@supports () { } @supports not () { }');
_exerciseAllEntryPoints('@supports (a: 1) and () { }');
@@ -457,6 +466,7 @@
test('malformed selectors, namespace selectors, and selector expressions',
() {
_exerciseAllEntryPoints('ns| { color: red; } | { color: blue; }');
+ _exerciseAllEntryPoints('ns|\uFFFD { color: red; }');
_exerciseAllEntryPoints(
':not(ns|a) { color: red; } :not(ns|) { color: blue; }');
expect(NamespaceSelector(null, '', null).clone().toString(), '|');
@@ -478,6 +488,18 @@
_exerciseAllEntryPoints(
'a { color: #@foo; width: (@foo); height: [@foo]; top: @; }');
_exerciseAllEntryPoints('a { color: var(); background: var(123); }');
+ final errors = <Message>[];
+ parse(
+ '@\uFFFD { }',
+ errors: errors,
+ options: const PreprocessorOptions(
+ useColors: false,
+ checked: true,
+ lessSupport: false,
+ inputFile: 'memory',
+ ),
+ );
+ expect(errors, isNotEmpty);
});
test('nested selectors inside directives and cyclic @extend / var()', () {
@@ -489,6 +511,7 @@
_exerciseAllEntryPoints(
'@media screen { @media print { a { b { color: red; } } } }');
_exerciseAllEntryPoints('a { @extend .b; } .b { @extend a; }');
+ _exerciseAllEntryPoints('@namespefix; @name @name;');
_exerciseAllEntryPoints('var-a: var(a); var-b: var(c); var-c: var(b); '
'a { color: var(a); width: var(b, var(c)); '
'height: var(u, 1px var(a)); }');