@ordojs/core
Version:
Core compiler and runtime for OrdoJS framework
486 lines (426 loc) • 13.4 kB
text/typescript
/**
* @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;
}
}