Skip to content

Commit bafafe9

Browse files
committed
feat: Add location information to pseudo-class and pseudo-element error messages
This commit enhances error messages for unknown pseudo-classes and pseudo-elements by including the CSS Level or CSS Module where they are defined. Key changes: - Created `PseudoLocationIndex` interface to map pseudo-classes and pseudo-elements to their sources - Implemented `buildPseudoLocationIndex()` to pre-build an index of locations - Updated error handling in parser to include location information - Added test cases to verify the new error message functionality The changes provide more informative error messages, helping developers quickly identify where a specific pseudo-class or pseudo-element is defined.
1 parent 6d7a116 commit bafafe9

File tree

3 files changed

+195
-7
lines changed

3 files changed

+195
-7
lines changed

src/parser.ts

+31-7
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ import {
2929
getXmlOptions,
3030
SyntaxDefinition,
3131
CssModule,
32-
cssModules
32+
cssModules,
33+
pseudoLocationIndex
3334
} from './syntax-definitions.js';
3435
import {digitsChars, isHex, isIdent, isIdentStart, maxHexLength, quoteChars, whitespaceChars} from './utils.js';
3536

@@ -764,12 +765,23 @@ export function createParser(
764765
assert(isDoubleColon || pseudoName, 'Expected pseudo-class name.');
765766
assert(!isDoubleColon || pseudoName, 'Expected pseudo-element name.');
766767
assert(pseudoName, 'Expected pseudo-class name.');
767-
assert(
768+
if (
768769
!isDoubleColon ||
769-
pseudoElementsAcceptUnknown ||
770-
Object.prototype.hasOwnProperty.call(pseudoElementsDefinitions, pseudoName),
771-
`Unknown pseudo-element "${pseudoName}".`
772-
);
770+
pseudoElementsAcceptUnknown ||
771+
Object.prototype.hasOwnProperty.call(pseudoElementsDefinitions, pseudoName)
772+
) {
773+
// All good
774+
} else {
775+
// Generate a helpful error message with location information
776+
const locations = pseudoLocationIndex.pseudoElements[pseudoName];
777+
let errorMessage = `Unknown pseudo-element "${pseudoName}"`;
778+
779+
if (locations && locations.length > 0) {
780+
errorMessage += `. It is defined in: ${locations.join(', ')}`;
781+
}
782+
783+
fail(errorMessage + '.');
784+
}
773785

774786
isPseudoElement =
775787
pseudoElementsEnabled &&
@@ -800,7 +812,19 @@ export function createParser(
800812
assert(pseudoClassesEnabled, 'Pseudo-classes are not enabled.');
801813
const signature =
802814
pseudoClassesDefinitions[pseudoName] ?? (pseudoClassesAcceptUnknown && defaultPseudoSignature);
803-
assert(signature, `Unknown pseudo-class: "${pseudoName}".`);
815+
if (signature) {
816+
// All good
817+
} else {
818+
// Generate a helpful error message with location information
819+
const locations = pseudoLocationIndex.pseudoClasses[pseudoName];
820+
let errorMessage = `Unknown pseudo-class: "${pseudoName}"`;
821+
822+
if (locations && locations.length > 0) {
823+
errorMessage += `. It is defined in: ${locations.join(', ')}`;
824+
}
825+
826+
fail(errorMessage + '.');
827+
}
804828

805829
const argument = parsePseudoArgument(pseudoName, 'pseudo-class', signature);
806830
const pseudoClass: AstPseudoClass = {

src/syntax-definitions.ts

+124
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,130 @@ export const cssModules = {
502502
*/
503503
export type CssModule = keyof typeof cssModules;
504504

505+
/**
506+
* Maps pseudo-classes and pseudo-elements to their CSS Level or CSS Module
507+
*/
508+
export interface PseudoLocationIndex {
509+
pseudoClasses: Record<string, string[]>;
510+
pseudoElements: Record<string, string[]>;
511+
}
512+
513+
/**
514+
* Builds an index of where each pseudo-class and pseudo-element is defined
515+
* (in which CSS Level or CSS Module)
516+
*/
517+
export function buildPseudoLocationIndex(): PseudoLocationIndex {
518+
const index: PseudoLocationIndex = {
519+
pseudoClasses: {},
520+
pseudoElements: {}
521+
};
522+
523+
// Add CSS Levels (excluding 'latest' and 'progressive')
524+
const cssLevels: CssLevel[] = ['css1', 'css2', 'css3', 'selectors-3', 'selectors-4'];
525+
526+
for (const level of cssLevels) {
527+
const syntax = cssSyntaxDefinitions[level];
528+
529+
// Process pseudo-classes
530+
if (syntax.pseudoClasses && typeof syntax.pseudoClasses === 'object') {
531+
const { definitions } = syntax.pseudoClasses;
532+
if (definitions) {
533+
for (const [type, names] of Object.entries(definitions)) {
534+
for (const name of names) {
535+
if (!index.pseudoClasses[name]) {
536+
index.pseudoClasses[name] = [];
537+
}
538+
if (!index.pseudoClasses[name].includes(level)) {
539+
index.pseudoClasses[name].push(level);
540+
}
541+
}
542+
}
543+
}
544+
}
545+
546+
// Process pseudo-elements
547+
if (syntax.pseudoElements && typeof syntax.pseudoElements === 'object') {
548+
const { definitions } = syntax.pseudoElements;
549+
if (definitions) {
550+
if (Array.isArray(definitions)) {
551+
for (const name of definitions) {
552+
if (!index.pseudoElements[name]) {
553+
index.pseudoElements[name] = [];
554+
}
555+
if (!index.pseudoElements[name].includes(level)) {
556+
index.pseudoElements[name].push(level);
557+
}
558+
}
559+
} else {
560+
for (const [type, names] of Object.entries(definitions)) {
561+
for (const name of names) {
562+
if (!index.pseudoElements[name]) {
563+
index.pseudoElements[name] = [];
564+
}
565+
if (!index.pseudoElements[name].includes(level)) {
566+
index.pseudoElements[name].push(level);
567+
}
568+
}
569+
}
570+
}
571+
}
572+
}
573+
}
574+
575+
// Add CSS Modules
576+
for (const [moduleName, moduleSyntax] of Object.entries(cssModules)) {
577+
// Process pseudo-classes
578+
if (moduleSyntax.pseudoClasses && typeof moduleSyntax.pseudoClasses === 'object') {
579+
const { definitions } = moduleSyntax.pseudoClasses;
580+
if (definitions) {
581+
for (const [type, names] of Object.entries(definitions)) {
582+
for (const name of names) {
583+
if (!index.pseudoClasses[name]) {
584+
index.pseudoClasses[name] = [];
585+
}
586+
if (!index.pseudoClasses[name].includes(moduleName)) {
587+
index.pseudoClasses[name].push(moduleName);
588+
}
589+
}
590+
}
591+
}
592+
}
593+
594+
// Process pseudo-elements
595+
if (moduleSyntax.pseudoElements && typeof moduleSyntax.pseudoElements === 'object') {
596+
const { definitions } = moduleSyntax.pseudoElements;
597+
if (definitions) {
598+
if (Array.isArray(definitions)) {
599+
for (const name of definitions) {
600+
if (!index.pseudoElements[name]) {
601+
index.pseudoElements[name] = [];
602+
}
603+
if (!index.pseudoElements[name].includes(moduleName)) {
604+
index.pseudoElements[name].push(moduleName);
605+
}
606+
}
607+
} else {
608+
for (const [type, names] of Object.entries(definitions)) {
609+
for (const name of names) {
610+
if (!index.pseudoElements[name]) {
611+
index.pseudoElements[name] = [];
612+
}
613+
if (!index.pseudoElements[name].includes(moduleName)) {
614+
index.pseudoElements[name].push(moduleName);
615+
}
616+
}
617+
}
618+
}
619+
}
620+
}
621+
}
622+
623+
return index;
624+
}
625+
626+
// Pre-build the index for faster lookup
627+
export const pseudoLocationIndex = buildPseudoLocationIndex();
628+
505629
const latestSyntaxDefinition = {
506630
...selectors4SyntaxDefinition,
507631
modules: (Object.entries(cssModules) as [CssModule, SyntaxDefinition & {latest?: boolean}][])

test/modules.test.ts

+40
Original file line numberDiff line numberDiff line change
@@ -713,5 +713,45 @@ describe('CSS Modules', () => {
713713
})
714714
);
715715
});
716+
717+
it('should provide helpful error messages with location information', () => {
718+
const parse = createParser({
719+
syntax: {
720+
pseudoClasses: {
721+
unknown: 'reject'
722+
},
723+
pseudoElements: {
724+
unknown: 'reject'
725+
}
726+
}
727+
});
728+
729+
// Test for pseudo-class defined in a CSS module
730+
try {
731+
parse(':sticky');
732+
fail('Should have thrown an error');
733+
} catch (e) {
734+
expect(e.message).toContain('Unknown pseudo-class: "sticky"');
735+
expect(e.message).toContain('css-position-3');
736+
}
737+
738+
// Test for pseudo-element defined in a CSS module
739+
try {
740+
parse('::part(button)');
741+
fail('Should have thrown an error');
742+
} catch (e) {
743+
expect(e.message).toContain('Unknown pseudo-element "part"');
744+
expect(e.message).toContain('css-shadow-parts-1');
745+
}
746+
747+
// Test for pseudo-class defined in a CSS level
748+
try {
749+
parse(':focus-visible');
750+
fail('Should have thrown an error');
751+
} catch (e) {
752+
expect(e.message).toContain('Unknown pseudo-class: "focus-visible"');
753+
expect(e.message).toContain('selectors-4');
754+
}
755+
});
716756
});
717757
});

0 commit comments

Comments
 (0)