@ordojs/core
Version:
Core compiler and runtime for OrdoJS framework
1,285 lines (1,054 loc) • 36 kB
text/typescript
/**
* @fileoverview OrdoJS Parser - Refactored modular implementation
* @author OrdoJS Framework Team
*/
import {
ExpressionType,
LifecycleType,
StatementType,
SyntaxError,
TokenType,
type AttributeNode,
type ClientBlockNode,
type ComponentAST,
type ComponentNode,
type ExpressionNode,
type FunctionNode,
type HTMLElementNode,
type InterpolationNode,
type LifecycleHookNode,
type MarkupBlockNode,
type ParameterNode,
type PropDefinition,
type ReactiveVariableNode,
type ServerBlockNode,
type ServerFunctionNode,
type SourcePosition,
type StatementNode,
type TextNode,
type Token,
type TokenStream,
type TypeAnnotation
} from '../types/index.js';
/**
* Parser configuration options
*/
export interface ParserOptions {
/** Enable error recovery mode */
allowRecovery?: boolean;
/** Maximum number of errors before stopping */
maxErrors?: number;
/** Enable strict mode parsing */
strictMode?: boolean;
/** Generate source maps */
generateSourceMaps?: boolean;
/** Custom AST node processors */
nodeProcessors?: ASTNodeProcessor[];
/** Enable experimental features */
experimentalFeatures?: string[];
}
/**
* AST node processor for plugin-based AST transformation
*/
export interface ASTNodeProcessor {
/** Processor name */
name: string;
/** Node types this processor handles */
handles: string[];
/** Process AST node and return modified node */
process<T extends { type: string }>(node: T, context: ParserContext): T;
}
/**
* Parser context for tracking state during parsing
*/
export interface ParserContext {
/** Current component being parsed */
component?: ComponentNode;
/** Current block context */
blockContext: 'component' | 'client' | 'server' | 'markup';
/** Symbol table for scope tracking */
symbolTable: Map<string, SymbolInfo>;
/** Current scope depth */
scopeDepth: number;
/** Parser options */
options: Required<ParserOptions>;
}
/**
* Symbol information for scope tracking
*/
export interface SymbolInfo {
name: string;
type: 'variable' | 'function' | 'parameter' | 'prop';
dataType?: TypeAnnotation;
scope: number;
position: SourcePosition;
isConst?: boolean;
isPublic?: boolean;
}
/**
* Parser state for error recovery and debugging
*/
export interface ParserState {
tokenStream: TokenStream;
current: number;
filename: string;
errors: SyntaxError[];
warnings: SyntaxError[];
context: ParserContext;
}
/**
* Expression precedence levels for operator precedence parsing
*/
enum Precedence {
NONE = 0,
ASSIGNMENT = 1, // =
OR = 2, // ||
AND = 3, // &&
EQUALITY = 4, // == !=
COMPARISON = 5, // < > <= >=
TERM = 6, // + -
FACTOR = 7, // * / %
UNARY = 8, // ! -
CALL = 9, // . ()
PRIMARY = 10
}
/**
* Parse rule for expression parsing
*/
interface ParseRule {
prefix?: (parser: OrdoJSParser) => ExpressionNode;
infix?: (parser: OrdoJSParser, left: ExpressionNode) => ExpressionNode;
precedence: Precedence;
}
/**
* Enhanced OrdoJS Parser with modular architecture
*/
export class OrdoJSParser {
private state: ParserState;
private options: Required<ParserOptions>;
private parseRules: Map<TokenType, ParseRule>;
constructor(tokenStream: TokenStream, options: ParserOptions = {}, filename: string = 'unknown') {
this.options = {
allowRecovery: false,
maxErrors: 10,
strictMode: true,
generateSourceMaps: true,
nodeProcessors: [],
experimentalFeatures: [],
...options
};
this.state = {
tokenStream,
current: 0,
filename,
errors: [],
warnings: [],
context: {
blockContext: 'component',
symbolTable: new Map(),
scopeDepth: 0,
options: this.options
}
};
this.parseRules = this.initializeParseRules();
}
/**
* Parse token stream into Component AST
*/
parse(): ComponentAST {
try {
const component = this.parseComponent();
if (this.state.errors.length > 0 && !this.options.allowRecovery) {
throw this.state.errors[0];
}
// Process AST through registered processors
this.processAST(component);
return {
component,
dependencies: this.extractDependencies(component),
exports: this.extractExports(component),
sourceMap: this.options.generateSourceMaps ? this.generateSourceMap() : {
version: 3,
sources: [this.state.filename],
names: [],
mappings: '',
sourcesContent: []
}
};
} catch (error) {
if (error instanceof SyntaxError) {
throw error;
}
throw new SyntaxError(
`Parser error: ${error instanceof Error ? error.message : 'Unknown error'}`,
this.getCurrentPosition(),
['valid component syntax'],
this.peek().value,
this.state.filename
);
}
}
/**
* Get all errors encountered during parsing
*/
getErrors(): SyntaxError[] {
return [...this.state.errors];
}
/**
* Get all warnings encountered during parsing
*/
getWarnings(): SyntaxError[] {
return [...this.state.warnings];
}
/**
* Add custom AST node processor
*/
addNodeProcessor(processor: ASTNodeProcessor): void {
this.options.nodeProcessors.push(processor);
}
/**
* Remove AST node processor by name
*/
removeNodeProcessor(name: string): boolean {
const index = this.options.nodeProcessors.findIndex(p => p.name === name);
if (index >= 0) {
this.options.nodeProcessors.splice(index, 1);
return true;
}
return false;
}
private initializeParseRules(): Map<TokenType, ParseRule> {
const rules = new Map<TokenType, ParseRule>();
// Literals
rules.set(TokenType.BOOLEAN, { prefix: this.parseLiteral.bind(this), precedence: Precedence.NONE });
rules.set(TokenType.NUMBER, { prefix: this.parseLiteral.bind(this), precedence: Precedence.NONE });
rules.set(TokenType.STRING, { prefix: this.parseLiteral.bind(this), precedence: Precedence.NONE });
rules.set(TokenType.IDENTIFIER, { prefix: this.parseIdentifier.bind(this), precedence: Precedence.NONE });
// Grouping
rules.set(TokenType.LEFT_PAREN, { prefix: this.parseGrouping.bind(this), precedence: Precedence.NONE });
// Unary operators
rules.set(TokenType.LOGICAL_NOT, { prefix: this.parseUnary.bind(this), precedence: Precedence.NONE });
rules.set(TokenType.MINUS, { prefix: this.parseUnary.bind(this), infix: this.parseBinary.bind(this), precedence: Precedence.TERM });
// Binary operators
rules.set(TokenType.PLUS, { infix: this.parseBinary.bind(this), precedence: Precedence.TERM });
rules.set(TokenType.MULTIPLY, { infix: this.parseBinary.bind(this), precedence: Precedence.FACTOR });
rules.set(TokenType.DIVIDE, { infix: this.parseBinary.bind(this), precedence: Precedence.FACTOR });
rules.set(TokenType.MODULO, { infix: this.parseBinary.bind(this), precedence: Precedence.FACTOR });
// Comparison operators
rules.set(TokenType.EQUALS, { infix: this.parseBinary.bind(this), precedence: Precedence.EQUALITY });
rules.set(TokenType.NOT_EQUALS, { infix: this.parseBinary.bind(this), precedence: Precedence.EQUALITY });
rules.set(TokenType.LESS_THAN, { infix: this.parseBinary.bind(this), precedence: Precedence.COMPARISON });
rules.set(TokenType.GREATER_THAN, { infix: this.parseBinary.bind(this), precedence: Precedence.COMPARISON });
rules.set(TokenType.LESS_EQUAL, { infix: this.parseBinary.bind(this), precedence: Precedence.COMPARISON });
rules.set(TokenType.GREATER_EQUAL, { infix: this.parseBinary.bind(this), precedence: Precedence.COMPARISON });
// Logical operators
rules.set(TokenType.LOGICAL_AND, { infix: this.parseBinary.bind(this), precedence: Precedence.AND });
rules.set(TokenType.LOGICAL_OR, { infix: this.parseBinary.bind(this), precedence: Precedence.OR });
// Assignment
rules.set(TokenType.ASSIGN, { infix: this.parseAssignment.bind(this), precedence: Precedence.ASSIGNMENT });
// Call and member access
rules.set(TokenType.LEFT_PAREN, { infix: this.parseCall.bind(this), precedence: Precedence.CALL });
rules.set(TokenType.DOT, { infix: this.parseMember.bind(this), precedence: Precedence.CALL });
return rules;
}
private parseComponent(): ComponentNode {
const start = this.getCurrentPosition();
this.state.context.blockContext = 'component';
// Expect 'component' keyword
if (!this.match(TokenType.COMPONENT)) {
this.throwError("Expected 'component' keyword", ['component']);
}
// Parse component name
const nameToken = this.consume(TokenType.IDENTIFIER, 'Expected component name');
const name = nameToken.value;
// Validate component name
if (!this.isValidComponentName(name)) {
this.throwError(
'Component name must start with uppercase letter and contain only alphanumeric characters',
[]
);
}
// Add component to symbol table
this.addSymbol(name, 'function', undefined, this.getCurrentPosition());
// Parse optional props
const props = this.parseProps();
// Expect component body
this.consume(TokenType.LEFT_BRACE, "Expected '{' after component declaration");
this.enterScope();
// Parse component blocks
let clientBlock: ClientBlockNode | undefined;
let serverBlock: ServerBlockNode | undefined;
let markupBlock: MarkupBlockNode | undefined;
while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) {
if (this.match(TokenType.CLIENT)) {
if (clientBlock) {
this.throwError('Duplicate client block', []);
}
clientBlock = this.parseClientBlock();
} else if (this.match(TokenType.SERVER)) {
if (serverBlock) {
this.throwError('Duplicate server block', []);
}
serverBlock = this.parseServerBlock();
} else if (this.match(TokenType.MARKUP)) {
if (markupBlock) {
this.throwError('Duplicate markup block', []);
}
markupBlock = this.parseMarkupBlock();
} else {
this.throwError('Expected client, server, or markup block', ['client', 'server', 'markup']);
}
}
this.exitScope();
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after component body");
// Markup block is required
if (!markupBlock) {
this.throwError('Component must have a markup block', ['markup']);
}
const end = this.getCurrentPosition();
const component: ComponentNode = {
type: 'Component',
name,
props,
clientBlock,
serverBlock,
markupBlock: markupBlock!,
range: { start, end }
};
this.state.context.component = component;
return component;
}
private parseProps(): PropDefinition[] {
const props: PropDefinition[] = [];
if (!this.check(TokenType.LEFT_PAREN)) {
return props;
}
this.advance(); // consume '('
while (!this.check(TokenType.RIGHT_PAREN) && !this.isAtEnd()) {
const prop = this.parseProp();
props.push(prop);
// Add prop to symbol table
this.addSymbol(prop.name, 'prop', prop.dataType, prop.range.start);
if (!this.match(TokenType.COMMA)) {
break;
}
}
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after props");
return props;
}
private parseProp(): PropDefinition {
const start = this.getCurrentPosition();
const nameToken = this.consume(TokenType.IDENTIFIER, 'Expected prop name');
this.consume(TokenType.COLON, "Expected ':' after prop name");
const dataType = this.parseTypeAnnotation();
let defaultValue: ExpressionNode | undefined;
let isRequired = true;
if (this.match(TokenType.ASSIGN)) {
isRequired = false;
defaultValue = this.parseExpression();
}
const end = this.getCurrentPosition();
return {
type: 'PropDefinition',
name: nameToken.value,
dataType,
defaultValue,
isRequired,
range: { start, end }
} as PropDefinition;
}
private parseTypeAnnotation(): TypeAnnotation {
const nameToken = this.consume(TokenType.IDENTIFIER, 'Expected type name');
let isArray = false;
let isOptional = false;
const genericTypes: TypeAnnotation[] = [];
// Handle array types
if (this.match(TokenType.LEFT_BRACKET)) {
this.consume(TokenType.RIGHT_BRACKET, "Expected ']' after '['");
isArray = true;
}
// Handle optional types
if (this.match(TokenType.QUESTION)) {
isOptional = true;
}
// Handle generic types (simplified)
if (this.match(TokenType.LESS_THAN)) {
do {
genericTypes.push(this.parseTypeAnnotation());
} while (this.match(TokenType.COMMA));
this.consume(TokenType.GREATER_THAN, "Expected '>' after generic types");
}
return {
name: nameToken.value,
isArray,
isOptional,
genericTypes
};
}
private parseClientBlock(): ClientBlockNode {
const start = this.getCurrentPosition();
this.state.context.blockContext = 'client';
this.consume(TokenType.LEFT_BRACE, "Expected '{' after 'client'");
this.enterScope();
const reactiveVariables: ReactiveVariableNode[] = [];
const computedValues: any[] = [];
const eventHandlers: any[] = [];
const functions: FunctionNode[] = [];
const lifecycle: LifecycleHookNode[] = [];
while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) {
if (this.check(TokenType.LET) || this.check(TokenType.CONST)) {
reactiveVariables.push(this.parseReactiveVariable());
} else if (this.checkLifecycleHook()) {
lifecycle.push(this.parseLifecycleHook());
} else if (this.check(TokenType.IDENTIFIER)) {
functions.push(this.parseFunction());
} else {
this.throwError('Expected variable declaration, function, or lifecycle hook',
['let', 'const', 'function', 'onMount', 'onUnmount', 'onUpdate']);
}
}
this.exitScope();
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after client block");
const end = this.getCurrentPosition();
return {
type: 'ClientBlock',
reactiveVariables,
computedValues,
eventHandlers,
functions,
lifecycle,
range: { start, end }
};
}
private parseServerBlock(): ServerBlockNode {
const start = this.getCurrentPosition();
this.state.context.blockContext = 'server';
this.consume(TokenType.LEFT_BRACE, "Expected '{' after 'server'");
this.enterScope();
const functions: ServerFunctionNode[] = [];
const middleware: any[] = [];
const dataFetchers: any[] = [];
const imports: any[] = [];
while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) {
if (this.check(TokenType.PUBLIC) || this.check(TokenType.IDENTIFIER)) {
functions.push(this.parseServerFunction());
} else {
this.throwError('Expected function declaration', ['public', 'function']);
}
}
this.exitScope();
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after server block");
const end = this.getCurrentPosition();
return {
type: 'ServerBlock',
functions,
middleware,
dataFetchers,
imports,
range: { start, end }
};
}
private parseMarkupBlock(): MarkupBlockNode {
const start = this.getCurrentPosition();
this.state.context.blockContext = 'markup';
this.consume(TokenType.LEFT_BRACE, "Expected '{' after 'markup'");
const elements: HTMLElementNode[] = [];
const textNodes: TextNode[] = [];
const interpolations: InterpolationNode[] = [];
while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) {
if (this.check(TokenType.HTML_TAG_OPEN)) {
elements.push(this.parseHTMLElement());
} else if (this.check(TokenType.INTERPOLATION_START)) {
interpolations.push(this.parseInterpolation());
} else if (this.check(TokenType.HTML_TEXT)) {
textNodes.push(this.parseTextNode());
} else {
// Skip unknown tokens in markup context
this.advance();
}
}
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after markup block");
const end = this.getCurrentPosition();
return {
type: 'MarkupBlock',
elements,
textNodes,
interpolations,
range: { start, end }
};
}
private parseReactiveVariable(): ReactiveVariableNode {
const start = this.getCurrentPosition();
const isConst = this.match(TokenType.CONST);
if (!isConst) {
this.consume(TokenType.LET, "Expected 'let' or 'const'");
}
const nameToken = this.consume(TokenType.IDENTIFIER, 'Expected variable name');
// Check for duplicate declaration
if (this.hasSymbolInCurrentScope(nameToken.value)) {
this.throwError(`Variable '${nameToken.value}' is already declared in this scope`, []);
}
let dataType: TypeAnnotation = { name: 'any', isArray: false, isOptional: false, genericTypes: [] };
if (this.match(TokenType.COLON)) {
dataType = this.parseTypeAnnotation();
}
this.consume(TokenType.ASSIGN, "Expected '=' after variable declaration");
const initialValue = this.parseExpression();
this.match(TokenType.SEMICOLON); // Optional semicolon
// Add variable to symbol table
this.addSymbol(nameToken.value, 'variable', dataType, start, isConst);
const end = this.getCurrentPosition();
return {
type: 'ReactiveVariable',
name: nameToken.value,
initialValue,
dataType,
isConst,
range: { start, end }
};
}
private parseFunction(): FunctionNode {
const start = this.getCurrentPosition();
const nameToken = this.consume(TokenType.IDENTIFIER, 'Expected function name');
// Check for duplicate declaration
if (this.hasSymbolInCurrentScope(nameToken.value)) {
this.throwError(`Function '${nameToken.value}' is already declared in this scope`, []);
}
this.consume(TokenType.LEFT_PAREN, "Expected '(' after function name");
this.enterScope();
const parameters = this.parseParameters();
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after parameters");
this.consume(TokenType.COLON, "Expected ':' after parameters");
const returnType = this.parseTypeAnnotation();
this.consume(TokenType.LEFT_BRACE, "Expected '{' after function signature");
const body = this.parseStatements();
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after function body");
this.exitScope();
// Add function to symbol table
this.addSymbol(nameToken.value, 'function', returnType, start);
const end = this.getCurrentPosition();
return {
type: 'Function',
name: nameToken.value,
parameters,
body,
returnType,
isAsync: false,
range: { start, end }
};
}
private parseServerFunction(): ServerFunctionNode {
const start = this.getCurrentPosition();
const isPublic = this.match(TokenType.PUBLIC);
const nameToken = this.consume(TokenType.IDENTIFIER, 'Expected function name');
// Check for duplicate declaration
if (this.hasSymbolInCurrentScope(nameToken.value)) {
this.throwError(`Function '${nameToken.value}' is already declared in this scope`, []);
}
this.consume(TokenType.LEFT_PAREN, "Expected '(' after function name");
this.enterScope();
const parameters = this.parseParameters();
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after parameters");
this.consume(TokenType.COLON, "Expected ':' after parameters");
const returnType = this.parseTypeAnnotation();
this.consume(TokenType.LEFT_BRACE, "Expected '{' after function signature");
const body = this.parseStatements();
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after function body");
this.exitScope();
// Add function to symbol table
this.addSymbol(nameToken.value, 'function', returnType, start, false, isPublic);
const end = this.getCurrentPosition();
return {
type: 'ServerFunction',
name: nameToken.value,
parameters,
body,
returnType,
isAsync: false,
isPublic,
middleware: [],
permissions: [],
range: { start, end }
};
}
private parseLifecycleHook(): LifecycleHookNode {
const start = this.getCurrentPosition();
const hookName = this.advance().value;
let hookType: LifecycleType;
switch (hookName) {
case 'onMount':
hookType = LifecycleType.ON_MOUNT;
break;
case 'onUnmount':
hookType = LifecycleType.ON_UNMOUNT;
break;
case 'onUpdate':
hookType = LifecycleType.ON_UPDATE;
break;
case 'beforeUpdate':
hookType = LifecycleType.BEFORE_UPDATE;
break;
case 'afterUpdate':
hookType = LifecycleType.AFTER_UPDATE;
break;
default:
this.throwError(`Unknown lifecycle hook: ${hookName}`,
['onMount', 'onUnmount', 'onUpdate', 'beforeUpdate', 'afterUpdate']);
hookType = LifecycleType.ON_MOUNT;
}
this.consume(TokenType.LEFT_PAREN, "Expected '(' after lifecycle hook");
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after lifecycle hook");
this.consume(TokenType.LEFT_BRACE, "Expected '{' after lifecycle hook");
this.enterScope();
const handler = this.parseStatements();
this.exitScope();
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after lifecycle hook body");
const end = this.getCurrentPosition();
return {
type: 'LifecycleHook',
hookType,
handler,
range: { start, end }
};
}
private parseParameters(): ParameterNode[] {
const parameters: ParameterNode[] = [];
while (!this.check(TokenType.RIGHT_PAREN) && !this.isAtEnd()) {
const param = this.parseParameter();
parameters.push(param);
// Add parameter to symbol table
this.addSymbol(param.name, 'parameter', param.dataType, param.range.start);
if (!this.match(TokenType.COMMA)) {
break;
}
}
return parameters;
}
private parseParameter(): ParameterNode {
const start = this.getCurrentPosition();
const nameToken = this.consume(TokenType.IDENTIFIER, 'Expected parameter name');
this.consume(TokenType.COLON, "Expected ':' after parameter name");
const dataType = this.parseTypeAnnotation();
let defaultValue: ExpressionNode | undefined;
let isOptional = dataType.isOptional;
if (this.match(TokenType.ASSIGN)) {
isOptional = true;
defaultValue = this.parseExpression();
}
const end = this.getCurrentPosition();
return {
type: 'Parameter',
name: nameToken.value,
dataType,
defaultValue,
isOptional,
range: { start, end }
} as ParameterNode;
}
private parseStatements(): StatementNode[] {
const statements: StatementNode[] = [];
while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) {
statements.push(this.parseStatement());
}
return statements;
}
private parseStatement(): StatementNode {
const start = this.getCurrentPosition();
// For now, treat everything as expression statements
const expression = this.parseExpression();
this.match(TokenType.SEMICOLON); // Optional semicolon
const end = this.getCurrentPosition();
return {
type: 'Statement',
statementType: StatementType.EXPRESSION,
expression,
range: { start, end }
};
}
private parseExpression(): ExpressionNode {
return this.parsePrecedence(Precedence.ASSIGNMENT);
}
private parsePrecedence(precedence: Precedence): ExpressionNode {
const prefixRule = this.parseRules.get(this.peek().type)?.prefix;
if (!prefixRule) {
this.throwError('Expected expression', ['identifier', 'number', 'string', 'boolean', '(']);
// Return dummy expression for error recovery
return this.createDummyExpression();
}
let left = prefixRule(this);
while (precedence <= this.getPrecedence()) {
const infixRule = this.parseRules.get(this.peek().type)?.infix;
if (!infixRule) break;
left = infixRule(this, left);
}
return left;
}
private getPrecedence(): Precedence {
const rule = this.parseRules.get(this.peek().type);
return rule?.precedence || Precedence.NONE;
}
// Expression parsing methods
private parseLiteral(): ExpressionNode {
const token = this.previous();
let value: any = token.value;
if (token.type === TokenType.BOOLEAN) {
value = token.value === 'true';
} else if (token.type === TokenType.NUMBER) {
value = parseFloat(token.value);
}
return {
type: 'Expression',
expressionType: ExpressionType.LITERAL,
value,
range: token.range
};
}
private parseIdentifier(): ExpressionNode {
const token = this.previous();
// Check if identifier is declared
if (this.options.strictMode && !this.hasSymbol(token.value)) {
this.addWarning(`Undefined variable: '${token.value}'`, token.range.start);
}
return {
type: 'Expression',
expressionType: ExpressionType.IDENTIFIER,
identifier: token.value,
range: token.range
};
}
private parseGrouping(): ExpressionNode {
const expr = this.parseExpression();
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after expression");
return expr;
}
private parseUnary(): ExpressionNode {
const operator = this.previous();
const right = this.parsePrecedence(Precedence.UNARY);
return {
type: 'Expression',
expressionType: ExpressionType.UNARY,
operator: operator.value,
right,
range: { start: operator.range.start, end: right.range.end }
};
}
private parseBinary(parser: OrdoJSParser, left: ExpressionNode): ExpressionNode {
const operator = this.previous();
const rule = this.parseRules.get(operator.type);
const right = this.parsePrecedence((rule?.precedence || Precedence.NONE) + 1);
return {
type: 'Expression',
expressionType: ExpressionType.BINARY,
operator: operator.value,
left,
right,
range: { start: left.range.start, end: right.range.end }
};
}
private parseAssignment(parser: OrdoJSParser, left: ExpressionNode): ExpressionNode {
const operator = this.previous();
const right = this.parseExpression();
return {
type: 'Expression',
expressionType: ExpressionType.ASSIGNMENT,
operator: operator.value,
left,
right,
range: { start: left.range.start, end: right.range.end }
};
}
private parseCall(parser: OrdoJSParser, left: ExpressionNode): ExpressionNode {
const args: ExpressionNode[] = [];
if (!this.check(TokenType.RIGHT_PAREN)) {
do {
args.push(this.parseExpression());
} while (this.match(TokenType.COMMA));
}
const paren = this.consume(TokenType.RIGHT_PAREN, "Expected ')' after arguments");
return {
type: 'Expression',
expressionType: ExpressionType.CALL,
callee: left,
arguments: args,
range: { start: left.range.start, end: paren.range.end }
};
}
private parseMember(parser: OrdoJSParser, left: ExpressionNode): ExpressionNode {
const name = this.consume(TokenType.IDENTIFIER, "Expected property name after '.'");
return {
type: 'Expression',
expressionType: ExpressionType.MEMBER,
object: left,
property: {
type: 'Expression',
expressionType: ExpressionType.IDENTIFIER,
identifier: name.value,
range: name.range
},
range: { start: left.range.start, end: name.range.end }
};
}
private parseHTMLElement(): HTMLElementNode {
const start = this.getCurrentPosition();
// Simplified HTML parsing - would need much more sophisticated implementation
const tagName = 'div'; // Placeholder
const attributes: AttributeNode[] = [];
const children: (HTMLElementNode | TextNode | InterpolationNode)[] = [];
const end = this.getCurrentPosition();
return {
type: 'HTMLElement',
tagName,
attributes,
children,
isSelfClosing: false,
isVoidElement: false,
range: { start, end }
};
}
private parseInterpolation(): InterpolationNode {
const start = this.getCurrentPosition();
this.consume(TokenType.INTERPOLATION_START, "Expected '{' for interpolation");
const expression = this.parseExpression();
this.consume(TokenType.INTERPOLATION_END, "Expected '}' after interpolation");
const end = this.getCurrentPosition();
return {
type: 'Interpolation',
expression,
range: { start, end }
};
}
private parseTextNode(): TextNode {
const start = this.getCurrentPosition();
const content = this.consume(TokenType.HTML_TEXT, 'Expected text content').value;
const end = this.getCurrentPosition();
return {
type: 'Text',
content,
range: { start, end }
};
}
// Symbol table management
private enterScope(): void {
this.state.context.scopeDepth++;
}
private exitScope(): void {
// Remove symbols from current scope
for (const [name, symbol] of this.state.context.symbolTable) {
if (symbol.scope === this.state.context.scopeDepth) {
this.state.context.symbolTable.delete(name);
}
}
this.state.context.scopeDepth--;
}
private addSymbol(
name: string,
type: SymbolInfo['type'],
dataType: TypeAnnotation | undefined,
position: SourcePosition,
isConst: boolean = false,
isPublic: boolean = false
): void {
this.state.context.symbolTable.set(name, {
name,
type,
dataType,
scope: this.state.context.scopeDepth,
position,
isConst,
isPublic
});
}
private hasSymbol(name: string): boolean {
return this.state.context.symbolTable.has(name);
}
private hasSymbolInCurrentScope(name: string): boolean {
const symbol = this.state.context.symbolTable.get(name);
return symbol?.scope === this.state.context.scopeDepth;
}
private getSymbol(name: string): SymbolInfo | undefined {
return this.state.context.symbolTable.get(name);
}
// AST processing
private processAST(component: ComponentNode): void {
if (this.options.nodeProcessors.length === 0) return;
this.processNode(component);
}
private processNode(node: any): void {
for (const processor of this.options.nodeProcessors) {
if (processor.handles.includes(node.type)) {
node = processor.process(node, this.state.context);
}
}
// Recursively process child nodes
if (node.children) {
for (const child of node.children) {
this.processNode(child);
}
}
// Process specific node types
if (node.clientBlock) this.processNode(node.clientBlock);
if (node.serverBlock) this.processNode(node.serverBlock);
if (node.markupBlock) this.processNode(node.markupBlock);
if (node.functions) {
for (const func of node.functions) {
this.processNode(func);
}
}
if (node.reactiveVariables) {
for (const variable of node.reactiveVariables) {
this.processNode(variable);
}
}
}
// Utility methods
private checkLifecycleHook(): boolean {
const value = this.peek().value;
return ['onMount', 'onUnmount', 'onUpdate', 'beforeUpdate', 'afterUpdate'].includes(value);
}
private isValidComponentName(name: string): boolean {
return /^[A-Z][a-zA-Z0-9]*$/.test(name);
}
private createDummyExpression(): ExpressionNode {
return {
type: 'Expression',
expressionType: ExpressionType.LITERAL,
value: null,
range: { start: this.getCurrentPosition(), end: this.getCurrentPosition() }
};
}
private extractDependencies(component: ComponentNode): string[] {
// Extract import dependencies from the component
const dependencies: string[] = [];
// Implementation would analyze the AST for imports
return dependencies;
}
private extractExports(component: ComponentNode): string[] {
// Extract exports from the component
const exports: string[] = [component.name];
// Implementation would analyze the AST for exports
return exports;
}
private generateSourceMap(): any {
// Generate source map for debugging
return {
version: 3,
sources: [this.state.filename],
names: [],
mappings: '',
sourcesContent: []
};
}
// Token stream utilities
private match(...types: TokenType[]): boolean {
for (const type of types) {
if (this.check(type)) {
this.advance();
return true;
}
}
return false;
}
private check(type: TokenType): boolean {
if (this.isAtEnd()) return false;
return this.peek().type === type;
}
private advance(): Token {
if (!this.isAtEnd()) this.state.current++;
return this.previous();
}
private isAtEnd(): boolean {
return this.peek().type === TokenType.EOF;
}
private peek(): Token {
return this.state.tokenStream.tokens[this.state.current] || this.createEOFToken();
}
private previous(): Token {
return this.state.tokenStream.tokens[this.state.current - 1] || this.createEOFToken();
}
private consume(type: TokenType, message: string): Token {
if (this.check(type)) return this.advance();
this.throwError(message, [type.toString()]);
// Return dummy token for error recovery
return this.createEOFToken();
}
private createEOFToken(): Token {
return {
type: TokenType.EOF,
value: '',
position: this.getCurrentPosition(),
range: { start: this.getCurrentPosition(), end: this.getCurrentPosition() }
};
}
private throwError(message: string, expected: string[]): never {
const error = new SyntaxError(
message,
this.getCurrentPosition(),
expected,
this.peek().value,
this.state.filename
);
if (this.options.allowRecovery && this.state.errors.length < this.options.maxErrors) {
this.state.errors.push(error);
this.synchronize();
throw error;
}
throw error;
}
private addWarning(message: string, position: SourcePosition): void {
const warning = new SyntaxError(
message,
position,
[],
'',
this.state.filename
);
this.state.warnings.push(warning);
}
private synchronize(): void {
this.advance();
while (!this.isAtEnd()) {
if (this.previous().type === TokenType.SEMICOLON) return;
switch (this.peek().type) {
case TokenType.COMPONENT:
case TokenType.CLIENT:
case TokenType.SERVER:
case TokenType.MARKUP:
case TokenType.LET:
case TokenType.CONST:
return;
}
this.advance();
}
}
private getCurrentPosition(): SourcePosition {
return this.peek().position;
}
}
/**
* Default AST node processors
*/
export const defaultNodeProcessors: ASTNodeProcessor[] = [
{
name: 'symbol-validator',
handles: ['Expression'],
process: <T extends { type: string }>(node: T, context: ParserContext): T => {
if (node.type === 'Expression') {
const expr = node as any as ExpressionNode;
if (expr.expressionType === ExpressionType.IDENTIFIER && expr.identifier) {
if (!context.symbolTable.has(expr.identifier)) {
// Could emit warning about undefined variable
}
}
}
return node;
}
},
{
name: 'type-checker',
handles: ['ReactiveVariable', 'Function', 'ServerFunction'],
process: (node: any, context: ParserContext): any => {
// Could perform type checking here
return node;
}
}
];