Skip to content

Commit

Permalink
Update specificity calculation for selector pseudos (#1781)
Browse files Browse the repository at this point in the history
This is very close to invisible to the user and actually making it
visible would require a complex and hard-to-read test, so I'm electing
to avoid testing it.

Closes #2528
  • Loading branch information
nex3 committed Aug 19, 2022
1 parent c850501 commit 7695332
Show file tree
Hide file tree
Showing 15 changed files with 58 additions and 147 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -6,6 +6,8 @@
* Properly consider `b > c` to be a superselector of `a > b > c`, and similarly
for other combinators.

* Properly calculate specificity for selector pseudoclasses.

* Deprecate use of `random()` when `$limit` has units to make it explicit that
`random()` currently ignores units. A future version will no longer ignore
units.
Expand Down
38 changes: 6 additions & 32 deletions lib/src/ast/selector/complex.dart
Expand Up @@ -45,27 +45,13 @@ class ComplexSelector extends Selector {
@internal
final bool lineBreak;

/// The minimum possible specificity that this selector can have.
/// This selector's specificity.
///
/// Pseudo selectors that contain selectors, like `:not()` and `:matches()`,
/// can have a range of possible specificities.
int get minSpecificity {
if (_minSpecificity == null) _computeSpecificity();
return _minSpecificity!;
}

int? _minSpecificity;

/// The maximum possible specificity that this selector can have.
///
/// Pseudo selectors that contain selectors, like `:not()` and `:matches()`,
/// can have a range of possible specificities.
int get maxSpecificity {
if (_maxSpecificity == null) _computeSpecificity();
return _maxSpecificity!;
}

int? _maxSpecificity;
/// Specificity is represented in base 1000. The spec says this should be
/// "sufficiently high"; it's extremely unlikely that any single selector
/// sequence will contain 1000 simple selectors.
late final int specificity = components.fold(
0, (sum, component) => sum + component.selector.specificity);

/// If this compound selector is composed of a single compound selector with
/// no combinators, returns it.
Expand Down Expand Up @@ -115,18 +101,6 @@ class ComplexSelector extends Selector {
other.leadingCombinators.isEmpty &&
complexIsSuperselector(components, other.components);

/// Computes [_minSpecificity] and [_maxSpecificity].
void _computeSpecificity() {
var minSpecificity = 0;
var maxSpecificity = 0;
for (var component in components) {
minSpecificity += component.selector.minSpecificity;
maxSpecificity += component.selector.maxSpecificity;
}
_minSpecificity = minSpecificity;
_maxSpecificity = maxSpecificity;
}

/// Returns a copy of `this` with [combinators] added to the end of the final
/// component in [components].
///
Expand Down
38 changes: 6 additions & 32 deletions lib/src/ast/selector/compound.dart
Expand Up @@ -25,27 +25,13 @@ class CompoundSelector extends Selector {
/// This is never empty.
final List<SimpleSelector> components;

/// The minimum possible specificity that this selector can have.
/// This selector's specificity.
///
/// Pseudo selectors that contain selectors, like `:not()` and `:matches()`,
/// can have a range of possible specificities.
int get minSpecificity {
if (_minSpecificity == null) _computeSpecificity();
return _minSpecificity!;
}

int? _minSpecificity;

/// The maximum possible specificity that this selector can have.
///
/// Pseudo selectors that contain selectors, like `:not()` and `:matches()`,
/// can have a range of possible specificities.
int get maxSpecificity {
if (_maxSpecificity == null) _computeSpecificity();
return _maxSpecificity!;
}

int? _maxSpecificity;
/// Specificity is represented in base 1000. The spec says this should be
/// "sufficiently high"; it's extremely unlikely that any single selector
/// sequence will contain 1000 simple selectors.
late final int specificity =
components.fold(0, (sum, component) => sum + component.specificity);

/// If this compound selector is composed of a single simple selector, returns
/// it.
Expand Down Expand Up @@ -87,18 +73,6 @@ class CompoundSelector extends Selector {
bool isSuperselector(CompoundSelector other) =>
compoundIsSuperselector(this, other);

/// Computes [_minSpecificity] and [_maxSpecificity].
void _computeSpecificity() {
var minSpecificity = 0;
var maxSpecificity = 0;
for (var simple in components) {
minSpecificity += simple.minSpecificity;
maxSpecificity += simple.maxSpecificity;
}
_minSpecificity = minSpecificity;
_maxSpecificity = maxSpecificity;
}

int get hashCode => listHash(components);

bool operator ==(Object other) =>
Expand Down
2 changes: 1 addition & 1 deletion lib/src/ast/selector/id.dart
Expand Up @@ -19,7 +19,7 @@ class IDSelector extends SimpleSelector {
/// The ID name this selects for.
final String name;

int get minSpecificity => math.pow(super.minSpecificity, 2) as int;
int get specificity => math.pow(super.specificity, 2) as int;

IDSelector(this.name);

Expand Down
77 changes: 25 additions & 52 deletions lib/src/ast/selector/pseudo.dart
Expand Up @@ -2,9 +2,8 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

import 'dart:math' as math;

import 'package:charcode/charcode.dart';
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';

import '../../utils.dart';
Expand Down Expand Up @@ -80,19 +79,30 @@ class PseudoSelector extends SimpleSelector {
/// both non-`null`, the selector follows the argument.
final SelectorList? selector;

int get minSpecificity {
if (_minSpecificity == null) _computeSpecificity();
return _minSpecificity!;
}

int? _minSpecificity;

int get maxSpecificity {
if (_maxSpecificity == null) _computeSpecificity();
return _maxSpecificity!;
}

int? _maxSpecificity;
late final int specificity = () {
if (isElement) return 1;
var selector = this.selector;
if (selector == null) return super.specificity;

// https://drafts.csswg.org/selectors/#specificity-rules
switch (normalizedName) {
case 'where':
return 0;
case 'is':
case 'not':
case 'has':
case 'matches':
return selector.components
.map((component) => component.specificity)
.max;
case 'nth-child':
case 'nth-last-child':
return super.specificity +
selector.components.map((component) => component.specificity).max;
default:
return super.specificity;
}
}();

PseudoSelector(this.name,
{bool element = false, this.argument, this.selector})
Expand Down Expand Up @@ -193,43 +203,6 @@ class PseudoSelector extends SimpleSelector {
return CompoundSelector([this]).isSuperselector(CompoundSelector([other]));
}

/// Computes [_minSpecificity] and [_maxSpecificity].
void _computeSpecificity() {
if (isElement) {
_minSpecificity = 1;
_maxSpecificity = 1;
return;
}

var selector = this.selector;
if (selector == null) {
_minSpecificity = super.minSpecificity;
_maxSpecificity = super.maxSpecificity;
return;
}

if (name == 'not') {
var minSpecificity = 0;
var maxSpecificity = 0;
for (var complex in selector.components) {
minSpecificity = math.max(minSpecificity, complex.minSpecificity);
maxSpecificity = math.max(maxSpecificity, complex.maxSpecificity);
}
_minSpecificity = minSpecificity;
_maxSpecificity = maxSpecificity;
} else {
// This is higher than any selector's specificity can actually be.
var minSpecificity = math.pow(super.minSpecificity, 3) as int;
var maxSpecificity = 0;
for (var complex in selector.components) {
minSpecificity = math.min(minSpecificity, complex.minSpecificity);
maxSpecificity = math.max(maxSpecificity, complex.maxSpecificity);
}
_minSpecificity = minSpecificity;
_maxSpecificity = maxSpecificity;
}
}

T accept<T>(SelectorVisitor<T> visitor) => visitor.visitPseudoSelector(this);

// This intentionally uses identity for the selector list, if one is available.
Expand Down
13 changes: 2 additions & 11 deletions lib/src/ast/selector/simple.dart
Expand Up @@ -27,21 +27,12 @@ final _subselectorPseudos = {
/// {@category AST}
/// {@category Parsing}
abstract class SimpleSelector extends Selector {
/// The minimum possible specificity that this selector can have.
///
/// Pseudo selectors that contain selectors, like `:not()` and `:matches()`,
/// can have a range of possible specificities.
/// This selector's specificity.
///
/// Specificity is represented in base 1000. The spec says this should be
/// "sufficiently high"; it's extremely unlikely that any single selector
/// sequence will contain 1000 simple selectors.
int get minSpecificity => 1000;

/// The maximum possible specificity that this selector can have.
///
/// Pseudo selectors that contain selectors, like `:not()` and `:matches()`,
/// can have a range of possible specificities.
int get maxSpecificity => minSpecificity;
int get specificity => 1000;

SimpleSelector();

Expand Down
2 changes: 1 addition & 1 deletion lib/src/ast/selector/type.dart
Expand Up @@ -18,7 +18,7 @@ class TypeSelector extends SimpleSelector {
/// The element name being selected.
final QualifiedName name;

int get minSpecificity => 1;
int get specificity => 1;

TypeSelector(this.name);

Expand Down
2 changes: 1 addition & 1 deletion lib/src/ast/selector/universal.dart
Expand Up @@ -21,7 +21,7 @@ class UniversalSelector extends SimpleSelector {
/// Otherwise, it matches all elements in the given namespace.
final String? namespace;

int get minSpecificity => 0;
int get specificity => 0;

UniversalSelector({this.namespace});

Expand Down
4 changes: 2 additions & 2 deletions lib/src/extend/extender.dart
Expand Up @@ -32,10 +32,10 @@ class Extender {

/// Creates a new extender.
///
/// If [specificity] isn't passed, it defaults to `extender.maxSpecificity`.
/// If [specificity] isn't passed, it defaults to `extender.specificity`.
Extender(this.selector, this.span,
{this.mediaContext, int? specificity, bool original = false})
: specificity = specificity ?? selector.maxSpecificity,
: specificity = specificity ?? selector.specificity,
isOriginal = original;

/// Asserts that the [mediaContext] for a selector is compatible with the
Expand Down
6 changes: 2 additions & 4 deletions lib/src/extend/extension.dart
Expand Up @@ -34,8 +34,6 @@ class Extension {
final FileSpan span;

/// Creates a new extension.
///
/// If [specificity] isn't passed, it defaults to `extender.maxSpecificity`.
Extension(
ComplexSelector extender, FileSpan extenderSpan, this.target, this.span,
{this.mediaContext, bool optional = false})
Expand Down Expand Up @@ -77,9 +75,9 @@ class Extender {

/// Creates a new extender.
///
/// If [specificity] isn't passed, it defaults to `extender.maxSpecificity`.
/// If [specificity] isn't passed, it defaults to `extender.specificity`.
Extender(this.selector, this.span, {int? specificity, bool original = false})
: specificity = specificity ?? selector.maxSpecificity,
: specificity = specificity ?? selector.specificity,
isOriginal = original;

/// Asserts that the [mediaContext] for a selector is compatible with the
Expand Down
6 changes: 3 additions & 3 deletions lib/src/extend/extension_store.dart
Expand Up @@ -261,7 +261,7 @@ class ExtensionStore {
_extensionsByExtender.putIfAbsent(simple, () => []).add(extension);
// Only source specificity for the original selector is relevant.
// Selectors generated by `@extend` don't get new specificity.
_sourceSpecificity.putIfAbsent(simple, () => complex.maxSpecificity);
_sourceSpecificity.putIfAbsent(simple, () => complex.specificity);
}

if (selectors != null || existingExtensions != null) {
Expand Down Expand Up @@ -970,13 +970,13 @@ class ExtensionStore {
// trimmed, and thus that if there are two identical selectors only one is
// trimmed.
if (result.any((complex2) =>
complex2.minSpecificity >= maxSpecificity &&
complex2.specificity >= maxSpecificity &&
complex2.isSuperselector(complex1))) {
continue;
}

if (selectors.take(i).any((complex2) =>
complex2.minSpecificity >= maxSpecificity &&
complex2.specificity >= maxSpecificity &&
complex2.isSuperselector(complex1))) {
continue;
}
Expand Down
6 changes: 2 additions & 4 deletions lib/src/extend/functions.dart
Expand Up @@ -657,10 +657,8 @@ bool complexIsSuperselector(List<ComplexSelectorComponent> complex1,
if (combinator1 == Combinator.followingSibling) {
// The selector `.foo ~ .bar` is only a superselector of selectors that
// *exclusively* contain subcombinators of `~`.
if (!complex2
.take(complex2.length - 1)
.skip(i2)
.every((component) => _isSupercombinator(
if (!complex2.take(complex2.length - 1).skip(i2).every((component) =>
_isSupercombinator(
combinator1, component.combinators.firstOrNull))) {
return false;
}
Expand Down
5 changes: 3 additions & 2 deletions pkg/sass_api/CHANGELOG.md
@@ -1,6 +1,7 @@
## 2.0.5
## 3.0.0

* No user-visible changes.
* Replace the `minSpecificity` and `maxSpecificity` fields on `ComplexSelector`,
`CompoundSelector`, and `SimpleSelector` with a single `specificity` field.

## 2.0.4

Expand Down
2 changes: 1 addition & 1 deletion pkg/sass_api/pubspec.yaml
Expand Up @@ -2,7 +2,7 @@ name: sass_api
# Note: Every time we add a new Sass AST node, we need to bump the *major*
# version because it's a breaking change for anyone who's implementing the
# visitor interface(s).
version: 2.0.5
version: 3.0.0
description: Additional APIs for Dart Sass.
homepage: https://github.com/sass/dart-sass

Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Expand Up @@ -15,7 +15,7 @@ dependencies:
async: ^2.5.0
charcode: ^1.2.0
cli_repl: ^0.2.1
collection: ^1.15.0
collection: ^1.16.0
meta: ^1.3.0
node_interop: ^2.1.0
js: ^0.6.3
Expand Down

0 comments on commit 7695332

Please sign in to comment.