From d20f690d3b8b8d87c16d80c0fc622991c612ecc8 Mon Sep 17 00:00:00 2001 From: Mridankan Mandal Date: Mon, 6 Apr 2026 00:34:36 +0530 Subject: [PATCH 1/8] fix: validate pseudo-class argument arity consistently Signed-off-by: Mridankan Mandal --- src/pseudo-selectors/index.ts | 30 ++++++++++++++++++++++++++++++ test/pseudo-classes.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/src/pseudo-selectors/index.ts b/src/pseudo-selectors/index.ts index a853144d..733725b9 100644 --- a/src/pseudo-selectors/index.ts +++ b/src/pseudo-selectors/index.ts @@ -20,6 +20,24 @@ import { filters } from "./filters.js"; import { pseudos, verifyPseudoArguments } from "./pseudos.js"; import { subselects } from "./subselects.js"; +const filtersWithArguments = new Set([ + "contains", + "icontains", + "nth-child", + "nth-last-child", + "nth-of-type", + "nth-last-of-type", + "lang", +]); + +const filtersWithoutArguments = new Set([ + "root", + "scope", + "hover", + "visited", + "active", +]); + /** * Compile a pseudo selector into an executable query function. * @param next Matcher to run after this matcher succeeds. @@ -37,6 +55,10 @@ export function compilePseudoSelector( ): CompiledQuery { const { name, data } = selector; + if (data === null && name in subselects) { + throw new Error(`Pseudo-class :${name} requires an argument`); + } + if (Array.isArray(data)) { if (!(name in subselects)) { throw new Error(`Unknown pseudo-class :${name}(${data})`); @@ -67,6 +89,14 @@ export function compilePseudoSelector( } if (name in filters) { + if (data === null && filtersWithArguments.has(name)) { + throw new Error(`Pseudo-class :${name} requires an argument`); + } + + if (data !== null && filtersWithoutArguments.has(name)) { + throw new Error(`Pseudo-class :${name} doesn't have any arguments`); + } + return filters[name]( next, data as string, diff --git a/test/pseudo-classes.ts b/test/pseudo-classes.ts index 92a58545..c1344644 100644 --- a/test/pseudo-classes.ts +++ b/test/pseudo-classes.ts @@ -127,6 +127,34 @@ describe("unmatched", () => { "Unknown pseudo-class :host-context", ); }); + + it("should throw when pseudo-classes are missing required arguments", () => { + expect(() => CSSselect.selectAll(":lang", dom)).toThrow( + "Pseudo-class :lang requires an argument", + ); + + expect(() => CSSselect.selectAll(":nth-child", dom)).toThrow( + "Pseudo-class :nth-child requires an argument", + ); + + expect(() => CSSselect.selectAll(":has", dom)).toThrow( + "Pseudo-class :has requires an argument", + ); + + expect(() => CSSselect.selectAll(":not", dom)).toThrow( + "Pseudo-class :not requires an argument", + ); + }); + + it("should throw when argument-less pseudo-classes receive arguments", () => { + expect(() => CSSselect.selectAll(":scope(foo)", dom)).toThrow( + "Pseudo-class :scope doesn't have any arguments", + ); + + expect(() => CSSselect.selectAll(":active(foo)", dom)).toThrow( + "Pseudo-class :active doesn't have any arguments", + ); + }); }); describe(":first-child", () => { From 488d27df51f7ba6b7600c51c1df94a4e748b0a40 Mon Sep 17 00:00:00 2001 From: Mridankan Mandal Date: Mon, 6 Apr 2026 00:45:38 +0530 Subject: [PATCH 2/8] test: align pseudo arity coverage with filter and subselect sets Signed-off-by: Mridankan Mandal --- src/pseudo-selectors/index.ts | 22 +++++++++++++++------- test/pseudo-classes.ts | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/pseudo-selectors/index.ts b/src/pseudo-selectors/index.ts index 733725b9..136a6d23 100644 --- a/src/pseudo-selectors/index.ts +++ b/src/pseudo-selectors/index.ts @@ -20,6 +20,8 @@ import { filters } from "./filters.js"; import { pseudos, verifyPseudoArguments } from "./pseudos.js"; import { subselects } from "./subselects.js"; +const allFilterNames = new Set(Object.keys(filters)); + const filtersWithArguments = new Set([ "contains", "icontains", @@ -30,13 +32,19 @@ const filtersWithArguments = new Set([ "lang", ]); -const filtersWithoutArguments = new Set([ - "root", - "scope", - "hover", - "visited", - "active", -]); +for (const filterName of filtersWithArguments) { + if (!allFilterNames.has(filterName)) { + throw new Error(`Unknown filter in filtersWithArguments: ${filterName}`); + } +} + +const filtersWithoutArguments = new Set( + [...allFilterNames].filter((filterName) => !filtersWithArguments.has(filterName)), +); + +if (filtersWithArguments.size + filtersWithoutArguments.size !== allFilterNames.size) { + throw new Error("Filter argument validation sets are out of sync with filters"); +} /** * Compile a pseudo selector into an executable query function. diff --git a/test/pseudo-classes.ts b/test/pseudo-classes.ts index c1344644..e8cf8202 100644 --- a/test/pseudo-classes.ts +++ b/test/pseudo-classes.ts @@ -129,6 +129,14 @@ describe("unmatched", () => { }); it("should throw when pseudo-classes are missing required arguments", () => { + expect(() => CSSselect.selectAll(":contains", dom)).toThrow( + "Pseudo-class :contains requires an argument", + ); + + expect(() => CSSselect.selectAll(":icontains", dom)).toThrow( + "Pseudo-class :icontains requires an argument", + ); + expect(() => CSSselect.selectAll(":lang", dom)).toThrow( "Pseudo-class :lang requires an argument", ); @@ -137,10 +145,34 @@ describe("unmatched", () => { "Pseudo-class :nth-child requires an argument", ); + expect(() => CSSselect.selectAll(":nth-last-child", dom)).toThrow( + "Pseudo-class :nth-last-child requires an argument", + ); + + expect(() => CSSselect.selectAll(":nth-of-type", dom)).toThrow( + "Pseudo-class :nth-of-type requires an argument", + ); + + expect(() => CSSselect.selectAll(":nth-last-of-type", dom)).toThrow( + "Pseudo-class :nth-last-of-type requires an argument", + ); + expect(() => CSSselect.selectAll(":has", dom)).toThrow( "Pseudo-class :has requires an argument", ); + expect(() => CSSselect.selectAll(":is", dom)).toThrow( + "Pseudo-class :is requires an argument", + ); + + expect(() => CSSselect.selectAll(":matches", dom)).toThrow( + "Pseudo-class :matches requires an argument", + ); + + expect(() => CSSselect.selectAll(":where", dom)).toThrow( + "Pseudo-class :where requires an argument", + ); + expect(() => CSSselect.selectAll(":not", dom)).toThrow( "Pseudo-class :not requires an argument", ); From 816ae4d74d21d0c379fd46a19b79f174efe55770 Mon Sep 17 00:00:00 2001 From: Mridankan Mandal Date: Mon, 6 Apr 2026 00:55:53 +0530 Subject: [PATCH 3/8] test: cover remaining argument-less pseudo validations Signed-off-by: Mridankan Mandal --- src/pseudo-selectors/index.ts | 4 ---- test/pseudo-classes.ts | 12 ++++++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/pseudo-selectors/index.ts b/src/pseudo-selectors/index.ts index 136a6d23..4464c7dc 100644 --- a/src/pseudo-selectors/index.ts +++ b/src/pseudo-selectors/index.ts @@ -42,10 +42,6 @@ const filtersWithoutArguments = new Set( [...allFilterNames].filter((filterName) => !filtersWithArguments.has(filterName)), ); -if (filtersWithArguments.size + filtersWithoutArguments.size !== allFilterNames.size) { - throw new Error("Filter argument validation sets are out of sync with filters"); -} - /** * Compile a pseudo selector into an executable query function. * @param next Matcher to run after this matcher succeeds. diff --git a/test/pseudo-classes.ts b/test/pseudo-classes.ts index e8cf8202..83ed8e67 100644 --- a/test/pseudo-classes.ts +++ b/test/pseudo-classes.ts @@ -186,6 +186,18 @@ describe("unmatched", () => { expect(() => CSSselect.selectAll(":active(foo)", dom)).toThrow( "Pseudo-class :active doesn't have any arguments", ); + + expect(() => CSSselect.selectAll(":root(foo)", dom)).toThrow( + "Pseudo-class :root doesn't have any arguments", + ); + + expect(() => CSSselect.selectAll(":hover(foo)", dom)).toThrow( + "Pseudo-class :hover doesn't have any arguments", + ); + + expect(() => CSSselect.selectAll(":visited(foo)", dom)).toThrow( + "Pseudo-class :visited doesn't have any arguments", + ); }); }); From 3f1e6ec2cd2d1ebbdcb04d3214c83d8717df19b9 Mon Sep 17 00:00:00 2001 From: Mridankan Mandal Date: Mon, 6 Apr 2026 01:18:58 +0530 Subject: [PATCH 4/8] fix: use own-property checks for pseudo maps Signed-off-by: Mridankan Mandal --- src/pseudo-selectors/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pseudo-selectors/index.ts b/src/pseudo-selectors/index.ts index 4464c7dc..ab3d8ca5 100644 --- a/src/pseudo-selectors/index.ts +++ b/src/pseudo-selectors/index.ts @@ -59,12 +59,12 @@ export function compilePseudoSelector( ): CompiledQuery { const { name, data } = selector; - if (data === null && name in subselects) { + if (data === null && Object.hasOwn(subselects, name)) { throw new Error(`Pseudo-class :${name} requires an argument`); } if (Array.isArray(data)) { - if (!(name in subselects)) { + if (!Object.hasOwn(subselects, name)) { throw new Error(`Unknown pseudo-class :${name}(${data})`); } @@ -92,7 +92,7 @@ export function compilePseudoSelector( return (element) => userPseudo(element, data) && next(element); } - if (name in filters) { + if (Object.hasOwn(filters, name)) { if (data === null && filtersWithArguments.has(name)) { throw new Error(`Pseudo-class :${name} requires an argument`); } @@ -110,7 +110,7 @@ export function compilePseudoSelector( ); } - if (name in pseudos) { + if (Object.hasOwn(pseudos, name)) { const pseudo = pseudos[name]; verifyPseudoArguments(pseudo, name, data, 2); From a7624bf97832eac04cad8f07b8e09cf6bf641477 Mon Sep 17 00:00:00 2001 From: Mridankan Mandal Date: Fri, 1 May 2026 14:10:03 +0530 Subject: [PATCH 5/8] fix: align alias pseudo arg errors Signed-off-by: Mridankan Mandal --- src/pseudo-selectors/index.ts | 4 +++- test/pseudo-classes.ts | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/pseudo-selectors/index.ts b/src/pseudo-selectors/index.ts index ab3d8ca5..40902961 100644 --- a/src/pseudo-selectors/index.ts +++ b/src/pseudo-selectors/index.ts @@ -78,7 +78,9 @@ export function compilePseudoSelector( if (typeof stringPseudo === "string") { if (data != null) { - throw new Error(`Pseudo ${name} doesn't have any arguments`); + throw new Error( + `Pseudo-class :${name} doesn't have any arguments`, + ); } // The alias has to be parsed here, to make sure options are respected. diff --git a/test/pseudo-classes.ts b/test/pseudo-classes.ts index 83ed8e67..e5679ab8 100644 --- a/test/pseudo-classes.ts +++ b/test/pseudo-classes.ts @@ -198,6 +198,10 @@ describe("unmatched", () => { expect(() => CSSselect.selectAll(":visited(foo)", dom)).toThrow( "Pseudo-class :visited doesn't have any arguments", ); + + expect(() => CSSselect.selectAll(":enabled(foo)", dom)).toThrow( + "Pseudo-class :enabled doesn't have any arguments", + ); }); }); From 33fd64b8835d4c51dd2abdcbdfebd3aad7bc49c4 Mon Sep 17 00:00:00 2001 From: Mridankan Mandal Date: Fri, 1 May 2026 14:44:54 +0530 Subject: [PATCH 6/8] test: cover remaining argument-less pseudos Signed-off-by: Mridankan Mandal --- test/pseudo-classes.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/pseudo-classes.ts b/test/pseudo-classes.ts index e5679ab8..1607081f 100644 --- a/test/pseudo-classes.ts +++ b/test/pseudo-classes.ts @@ -202,6 +202,18 @@ describe("unmatched", () => { expect(() => CSSselect.selectAll(":enabled(foo)", dom)).toThrow( "Pseudo-class :enabled doesn't have any arguments", ); + + expect(() => CSSselect.selectAll(":root(foo)", dom)).toThrow( + "Pseudo-class :root doesn't have any arguments", + ); + + expect(() => CSSselect.selectAll(":hover(foo)", dom)).toThrow( + "Pseudo-class :hover doesn't have any arguments", + ); + + expect(() => CSSselect.selectAll(":visited(foo)", dom)).toThrow( + "Pseudo-class :visited doesn't have any arguments", + ); }); }); From e64e99df67d4e749f235bfcd1aedda32a3955904 Mon Sep 17 00:00:00 2001 From: Mridankan Mandal Date: Fri, 1 May 2026 15:41:34 +0530 Subject: [PATCH 7/8] chore: retrigger CodeRabbit review Signed-off-by: Mridankan Mandal From 14350245b91baf5de2c20b65590b885302c1c3ac Mon Sep 17 00:00:00 2001 From: Mridankan Mandal Date: Fri, 1 May 2026 16:09:51 +0530 Subject: [PATCH 8/8] test: remove duplicate pseudo assertions Signed-off-by: Mridankan Mandal --- test/pseudo-classes.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/test/pseudo-classes.ts b/test/pseudo-classes.ts index 1607081f..e5679ab8 100644 --- a/test/pseudo-classes.ts +++ b/test/pseudo-classes.ts @@ -202,18 +202,6 @@ describe("unmatched", () => { expect(() => CSSselect.selectAll(":enabled(foo)", dom)).toThrow( "Pseudo-class :enabled doesn't have any arguments", ); - - expect(() => CSSselect.selectAll(":root(foo)", dom)).toThrow( - "Pseudo-class :root doesn't have any arguments", - ); - - expect(() => CSSselect.selectAll(":hover(foo)", dom)).toThrow( - "Pseudo-class :hover doesn't have any arguments", - ); - - expect(() => CSSselect.selectAll(":visited(foo)", dom)).toThrow( - "Pseudo-class :visited doesn't have any arguments", - ); }); });