UNPKG

@ordojs/core

Version:

Core compiler and runtime for OrdoJS framework

486 lines (426 loc) 13.4 kB
/** * @fileoverview OrdoJS Parser Fixes - Improvements for type safety and error handling */ import type { ASTNode, ComponentNode, ExpressionNode, HTMLElementNode, ReactiveVariableNode, SourcePosition, SourceRange, Token, TokenStream, TypeAnnotation } from '../types/index.js'; import { DirectiveType, ExpressionType, SyntaxError, TokenType } from '../types/index.js'; /** * Improved parser error handling and type safety */ export class ParserErrorHandler { /** * Create a properly typed syntax error with improved suggestions */ static createError( message: string, expected: string[], actual: string, position: SourcePosition, filename?: string ): SyntaxError { // Enhance error message with context let enhancedMessage = message; // Add specific suggestions based on error context const suggestions = ParserErrorHandler.generateSuggestions(expected, actual, message); return new SyntaxError( enhancedMessage, position, expected, actual, filename ); } /** * Generate helpful suggestions based on error context */ private static generateSuggestions(expected: string[], actual: string, message: string): string[] { const suggestions: string[] = []; // Component structure suggestions if (message.includes("component")) { suggestions.push("Make sure to start with 'component ComponentName {'"); suggestions.push("Component must have at least a markup block"); } // Block-specific suggestions if (message.includes("client") || message.includes("server") || message.includes("markup")) { suggestions.push("Each block type (client, server, markup) can only appear once"); suggestions.push("Blocks must be properly closed with '}'"); } // HTML-specific suggestions if (message.includes("tag")) { suggestions.push("Check for matching opening and closing tags"); suggestions.push("Self-closing tags should end with '/>'"); } // Expression suggestions if (message.includes("expression")) { suggestions.push("Check for balanced parentheses and operators"); suggestions.push("Verify variable names and property access syntax"); } return suggestions; } /** * Validate component structure */ static validateComponentStructure(component: ComponentNode): SyntaxError[] { const errors: SyntaxError[] = []; // Validate required markup block if (!component.markupBlock) { errors.push(new SyntaxError( "Component must have a markup block", component.range.start, ["markup"], "missing", "" )); } // Validate component name (must start with uppercase letter) if (!/^[A-Z][a-zA-Z0-9]*$/.test(component.name)) { errors.push(new SyntaxError( "Component name must start with uppercase letter and contain only alphanumeric characters", component.range.start, ["ValidComponentName"], component.name )); } // Validate no duplicate blocks const blockCounts = { client: 0, server: 0, markup: 0 }; component.children?.forEach(child => { if (child.type === 'ClientBlock') blockCounts.client++; if (child.type === 'ServerBlock') blockCounts.server++; if (child.type === 'MarkupBlock') blockCounts.markup++; }); if (blockCounts.client > 1) { errors.push(new SyntaxError( "Multiple client blocks found", component.range.start, ["single client block"], `${blockCounts.client} client blocks` )); } if (blockCounts.server > 1) { errors.push(new SyntaxError( "Multiple server blocks found", component.range.start, ["single server block"], `${blockCounts.server} server blocks` )); } if (blockCounts.markup > 1) { errors.push(new SyntaxError( "Multiple markup blocks found", component.range.start, ["single markup block"], `${blockCounts.markup} markup blocks` )); } return errors; } /** * Validate HTML structure */ static validateHTMLStructure(element: HTMLElementNode): SyntaxError[] { const errors: SyntaxError[] = []; // Check for void elements with children if (element.isVoidElement && element.children.length > 0) { errors.push(new SyntaxError( `Void element <${element.tagName}> cannot have children`, element.range.start, ["self-closing tag"], "tag with children" )); } // Check for duplicate ID attributes const idAttributes = element.attributes.filter(attr => attr.name === 'id'); if (idAttributes.length > 1) { errors.push(new SyntaxError( "Element cannot have multiple 'id' attributes", element.range.start, ["single id attribute"], `${idAttributes.length} id attributes` )); } // Recursively validate children element.children.forEach(child => { if (child.type === 'HTMLElement') { errors.push(...ParserErrorHandler.validateHTMLStructure(child as HTMLElementNode)); } }); return errors; } } /** * Null/undefined safety utilities for parser */ export class ParserSafetyUtils { /** * Safely access token value with null checking */ static safeTokenValue(token: Token | null | undefined): string { return token?.value || ''; } /** * Safely create a source range with null checking */ static safeCreateRange( start: SourcePosition | null | undefined, end: SourcePosition | null | undefined ): SourceRange { const defaultPos: SourcePosition = { line: 0, column: 0, offset: 0 }; return { start: start || defaultPos, end: end || defaultPos }; } /** * Safely handle optional children */ static safeChildren(nodes: (ASTNode | null | undefined)[]): ASTNode[] { return nodes.filter((node): node is ASTNode => node !== null && node !== undefined); } /** * Safely handle optional expressions */ static safeExpression(expr: ExpressionNode | null | undefined): ExpressionNode { if (expr) return expr; // Create a safe default expression const defaultPos: SourcePosition = { line: 0, column: 0, offset: 0 }; const defaultRange: SourceRange = { start: defaultPos, end: defaultPos }; return { type: 'Expression', expressionType: ExpressionType.LITERAL, value: null, range: defaultRange }; } /** * Safely handle optional type annotations */ static safeTypeAnnotation(type: TypeAnnotation | null | undefined): TypeAnnotation { return type || { name: 'any', isArray: false, isOptional: false, genericTypes: [] }; } } /** * Enhanced error recovery for parser */ export class ParserRecovery { /** * Synchronize parser state after error * @param tokens Token stream * @param synchronizationPoints Token types to synchronize on */ static synchronize( tokens: TokenStream, synchronizationPoints: TokenType[] = [ TokenType.SEMICOLON, TokenType.RIGHT_BRACE, TokenType.RIGHT_PAREN, TokenType.HTML_TAG_CLOSE ] ): void { // Skip tokens until we reach a synchronization point while (!tokens.isAtEnd()) { if (synchronizationPoints.includes(tokens.peek().type)) { tokens.advance(); // Consume the synchronization token return; } // Skip to next token tokens.advance(); } } /** * Attempt to recover from HTML parsing errors */ static recoverFromHTMLError( tokens: TokenStream, tagName: string ): void { // Skip until we find a matching closing tag or another opening tag while (!tokens.isAtEnd()) { const current = tokens.peek(); if (current.type === TokenType.HTML_TAG_OPEN) { // Found another opening tag, stop here return; } if (current.type === TokenType.HTML_TAG_CLOSE) { // Found a closing tag, consume it and return tokens.advance(); return; } // Skip to next token tokens.advance(); } } /** * Attempt to recover from block parsing errors */ static recoverFromBlockError(tokens: TokenStream): void { let braceCount = 0; // Skip until we find a matching closing brace while (!tokens.isAtEnd()) { const current = tokens.peek(); if (current.type === TokenType.LEFT_BRACE) { braceCount++; } else if (current.type === TokenType.RIGHT_BRACE) { braceCount--; if (braceCount <= 0) { tokens.advance(); // Consume the closing brace return; } } // Skip to next token tokens.advance(); } } } /** * Enhanced validation for parser */ export class ParserValidation { /** * Validate HTML element structure */ static validateHTMLElement(element: HTMLElementNode): SyntaxError[] { const errors: SyntaxError[] = []; // Check for void elements with closing tags const voidElements = [ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr' ]; if (voidElements.includes(element.tagName.toLowerCase()) && !element.isSelfClosing) { errors.push(new SyntaxError( `Void element <${element.tagName}> should be self-closing`, element.range.start, ["self-closing tag"], "non-self-closing tag" )); } // Check for invalid nesting const invalidNesting: Record<string, string[]> = { 'a': ['a'], 'button': ['button', 'input', 'select', 'textarea', 'a'], 'form': ['form'], 'label': ['label'], 'td': ['td', 'th', 'tr', 'thead', 'tfoot', 'tbody'], 'th': ['td', 'th', 'tr', 'thead', 'tfoot', 'tbody'] }; const tagName = element.tagName.toLowerCase(); if (invalidNesting[tagName]) { for (const child of element.children) { if (child.type === 'HTMLElement') { const childTag = (child as HTMLElementNode).tagName.toLowerCase(); if (invalidNesting[tagName].includes(childTag)) { errors.push(new SyntaxError( `Invalid nesting: <${childTag}> cannot be nested inside <${tagName}>`, child.range.start, ["valid nesting"], `<${tagName}> containing <${childTag}>` )); } } } } // Recursively validate children for (const child of element.children) { if (child.type === 'HTMLElement') { errors.push(...ParserValidation.validateHTMLElement(child as HTMLElementNode)); } } return errors; } /** * Validate directive usage */ static validateDirectives(element: HTMLElementNode): SyntaxError[] { const errors: SyntaxError[] = []; // Check for bind:value on non-input elements const bindValueAttrs = element.attributes.filter( attr => attr.isDirective && attr.directiveType === DirectiveType.BIND && attr.name === 'bind:value' ); if (bindValueAttrs.length > 0 && !['input', 'textarea', 'select'].includes(element.tagName.toLowerCase())) { errors.push(new SyntaxError( `bind:value can only be used on input, textarea, or select elements`, element.range.start, ["input", "textarea", "select"], element.tagName )); } // Check for duplicate event handlers const eventHandlers = element.attributes.filter( attr => attr.isDirective && attr.directiveType === DirectiveType.ON ); const eventNames = new Set<string>(); for (const handler of eventHandlers) { const eventName = handler.name.split(':')[1] || ''; if (eventName && eventNames.has(eventName)) { errors.push(new SyntaxError( `Duplicate event handler for '${eventName}'`, handler.range.start, ["single event handler"], "multiple handlers" )); } if (eventName) { eventNames.add(eventName); } } // Recursively validate children for (const child of element.children) { if (child.type === 'HTMLElement') { errors.push(...ParserValidation.validateDirectives(child as HTMLElementNode)); } } return errors; } /** * Validate reactive variable declarations */ static validateReactiveVariables(variables: ReactiveVariableNode[]): SyntaxError[] { const errors: SyntaxError[] = []; const declaredNames = new Set<string>(); for (const variable of variables) { // Check for duplicate declarations if (declaredNames.has(variable.name)) { errors.push(new SyntaxError( `Duplicate reactive variable declaration: '${variable.name}'`, variable.range.start, ["unique variable name"], variable.name )); } declaredNames.add(variable.name); // Check for const variables without initialization if (variable.isConst && !variable.initialValue) { errors.push(new SyntaxError( `Const variable '${variable.name}' must be initialized`, variable.range.start, ["initialization"], "missing initialization" )); } } return errors; } }