@ordojs/core
Version:
Core compiler and runtime for OrdoJS framework
324 lines • 12.9 kB
JavaScript
/**
* @fileoverview OrdoJS Parser Fixes - Improvements for type safety and error handling
*/
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, expected, actual, position, filename) {
// 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
*/
static generateSuggestions(expected, actual, message) {
const suggestions = [];
// 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) {
const errors = [];
// 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) {
const errors = [];
// 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));
}
});
return errors;
}
}
/**
* Null/undefined safety utilities for parser
*/
export class ParserSafetyUtils {
/**
* Safely access token value with null checking
*/
static safeTokenValue(token) {
return token?.value || '';
}
/**
* Safely create a source range with null checking
*/
static safeCreateRange(start, end) {
const defaultPos = { line: 0, column: 0, offset: 0 };
return {
start: start || defaultPos,
end: end || defaultPos
};
}
/**
* Safely handle optional children
*/
static safeChildren(nodes) {
return nodes.filter((node) => node !== null && node !== undefined);
}
/**
* Safely handle optional expressions
*/
static safeExpression(expr) {
if (expr)
return expr;
// Create a safe default expression
const defaultPos = { line: 0, column: 0, offset: 0 };
const defaultRange = { start: defaultPos, end: defaultPos };
return {
type: 'Expression',
expressionType: ExpressionType.LITERAL,
value: null,
range: defaultRange
};
}
/**
* Safely handle optional type annotations
*/
static safeTypeAnnotation(type) {
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, synchronizationPoints = [
TokenType.SEMICOLON,
TokenType.RIGHT_BRACE,
TokenType.RIGHT_PAREN,
TokenType.HTML_TAG_CLOSE
]) {
// 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, tagName) {
// 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) {
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) {
const errors = [];
// 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 = {
'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.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));
}
}
return errors;
}
/**
* Validate directive usage
*/
static validateDirectives(element) {
const errors = [];
// 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();
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));
}
}
return errors;
}
/**
* Validate reactive variable declarations
*/
static validateReactiveVariables(variables) {
const errors = [];
const declaredNames = new Set();
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;
}
}
//# sourceMappingURL=parser-fixes.js.map