From 3a46acda2eae472252152c9d6fc9344a20b6287d Mon Sep 17 00:00:00 2001 From: Bart Veneman Date: Sun, 8 Feb 2026 23:52:51 +0100 Subject: [PATCH] feat: analyze `@scope` --- src/atrules/atrules.test.ts | 52 +++++++++++++++++++++++++++++++++++++ src/index.test.ts | 6 +++++ src/index.ts | 4 +++ 3 files changed, 62 insertions(+) diff --git a/src/atrules/atrules.test.ts b/src/atrules/atrules.test.ts index a971333..689e32a 100644 --- a/src/atrules/atrules.test.ts +++ b/src/atrules/atrules.test.ts @@ -967,6 +967,58 @@ test('analyzes @property', () => { expect(actual).toEqual(expected) }) +test('analyzes @scope', () => { + // Examples from + // https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@scope#examples + // and + // https://drafts.csswg.org/css-cascade-6/#scoped-styles + const fixture = ` + @scope (.article-body) to (figure) {} + + @scope (.article-body) {} + + @scope (.article-body) to (:scope > figure) {} + + @scope (.article-body) to (.feature :scope figure) {} + + @scope (.media-object) to (.content > *) {} + + @scope ([data-scope='main-component']) to ([data-scope]) {} + + @scope ([data-scope='main-component']) to ([data-scope] > *) {} + + @scope (.parent-scope) { + @scope (:scope > .child-scope) to (:scope .limit) {} + } + + @scope (.parent-scope > .child-scope) to (.parent-scope > .child-scope .limit) {} + + /* No prelude */ + @scope {} + ` + const result = analyze(fixture) + const actual = result.atrules.scope + const expected = { + total: 10, + totalUnique: 10, + unique: { + '(.article-body) to (figure)': 1, + '(.article-body)': 1, + '(.article-body) to (:scope > figure)': 1, + '(.article-body) to (.feature :scope figure)': 1, + '(.media-object) to (.content > *)': 1, + "([data-scope='main-component']) to ([data-scope])": 1, + "([data-scope='main-component']) to ([data-scope] > *)": 1, + '(.parent-scope)': 1, + '(:scope > .child-scope) to (:scope .limit)': 1, + '(.parent-scope > .child-scope) to (.parent-scope > .child-scope .limit)': 1, + }, + uniquenessRatio: 1, + } + + expect(actual).toEqual(expected) +}) + test('tracks nesting depth', () => { const fixture = ` a { diff --git a/src/index.test.ts b/src/index.test.ts index 438c2a6..bf932ee 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -244,6 +244,12 @@ test('handles empty input gracefully', () => { unique: {}, uniquenessRatio: 0, }, + scope: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + }, complexity: { min: 0, max: 0, diff --git a/src/index.ts b/src/index.ts index ea856f3..2327c8b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -112,6 +112,7 @@ function analyzeInternal(css: string, options: Options, useLo let containers = new Collection(useLocations) let containerNames = new Collection(useLocations) let registeredProperties = new Collection(useLocations) + let scopes = new Collection(useLocations) let atruleNesting = new AggregateCollection() let uniqueAtruleNesting = new Collection(useLocations) @@ -294,6 +295,8 @@ function analyzeInternal(css: string, options: Options, useLo registeredProperties.p(node.prelude.text, toLoc(node)) } else if (normalized_name === 'charset') { charsets.p(node.prelude.text.toLowerCase(), toLoc(node)) + } else if (normalized_name === 'scope') { + scopes.p(node.prelude.text, toLoc(node)) } atRuleComplexities.push(complexity) @@ -858,6 +861,7 @@ function analyzeInternal(css: string, options: Options, useLo }), layer: layers.c(), property: registeredProperties.c(), + scope: scopes.c(), complexity: atRuleComplexity, nesting: assign( atruleNesting.aggregate(),