diff --git a/src/pseudo-selectors/index.ts b/src/pseudo-selectors/index.ts index a853144d..40902961 100644 --- a/src/pseudo-selectors/index.ts +++ b/src/pseudo-selectors/index.ts @@ -20,6 +20,28 @@ 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", + "nth-child", + "nth-last-child", + "nth-of-type", + "nth-last-of-type", + "lang", +]); + +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)), +); + /** * Compile a pseudo selector into an executable query function. * @param next Matcher to run after this matcher succeeds. @@ -37,8 +59,12 @@ export function compilePseudoSelector( ): CompiledQuery { const { name, data } = selector; + 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})`); } @@ -52,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. @@ -66,7 +94,15 @@ 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`); + } + + if (data !== null && filtersWithoutArguments.has(name)) { + throw new Error(`Pseudo-class :${name} doesn't have any arguments`); + } + return filters[name]( next, data as string, @@ -76,7 +112,7 @@ export function compilePseudoSelector( ); } - if (name in pseudos) { + if (Object.hasOwn(pseudos, name)) { const pseudo = pseudos[name]; verifyPseudoArguments(pseudo, name, data, 2); diff --git a/test/pseudo-classes.ts b/test/pseudo-classes.ts index 92a58545..e5679ab8 100644 --- a/test/pseudo-classes.ts +++ b/test/pseudo-classes.ts @@ -127,6 +127,82 @@ describe("unmatched", () => { "Unknown pseudo-class :host-context", ); }); + + 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", + ); + + expect(() => CSSselect.selectAll(":nth-child", dom)).toThrow( + "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", + ); + }); + + 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", + ); + + 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", + ); + + expect(() => CSSselect.selectAll(":enabled(foo)", dom)).toThrow( + "Pseudo-class :enabled doesn't have any arguments", + ); + }); }); describe(":first-child", () => {