UNPKG

@ordojs/core

Version:

Core compiler and runtime for OrdoJS framework

1,019 lines (897 loc) 27.3 kB
/** * @fileoverview Tests for OrdoJS Dependency Analyzer */ import { beforeEach, describe, expect, it } from 'vitest'; import { DirectiveType, ExpressionType, type AttributeNode, type ClientBlockNode, type ComponentAST, type ComponentNode, type ComputedValueNode, type ExpressionNode, type HTMLElementNode, type InterpolationNode, type MarkupBlockNode, type ReactiveVariableNode, type SourcePosition, type SourceRange } from '../types/index.js'; import { DependencyAnalyzer, DependencyType, UpdateType } from './dependency-analyzer.js'; describe('DependencyAnalyzer', () => { let analyzer: DependencyAnalyzer; let mockSourceRange: SourceRange; let mockSourcePosition: SourcePosition; beforeEach(() => { analyzer = new DependencyAnalyzer(); mockSourcePosition = { line: 1, column: 1, offset: 0 }; mockSourceRange = { start: mockSourcePosition, end: mockSourcePosition }; }); // Helper function to create a mock expression function createMockExpression( type: ExpressionType, identifier?: string, value?: any, left?: ExpressionNode, right?: ExpressionNode, operator?: string ): ExpressionNode { return { type: 'Expression', expressionType: type, identifier, value, left, right, operator, range: mockSourceRange }; } // Helper function to create a mock reactive variable function createMockReactiveVariable( name: string, initialValue: ExpressionNode, isConst = false ): ReactiveVariableNode { return { type: 'ReactiveVariable', name, initialValue, dataType: { name: 'any', isArray: false, isOptional: false, genericTypes: [] }, isConst, range: mockSourceRange }; } // Helper function to create a mock computed value function createMockComputedValue( name: string, expression: ExpressionNode, dependencies: string[] = [] ): ComputedValueNode { return { type: 'ComputedValue', name, expression, dependencies, dataType: { name: 'any', isArray: false, isOptional: false, genericTypes: [] }, range: mockSourceRange }; } // Helper function to create a mock interpolation function createMockInterpolation(expression: ExpressionNode): InterpolationNode { return { type: 'Interpolation', expression, range: mockSourceRange }; } // Helper function to create a mock attribute function createMockAttribute( name: string, value: string | ExpressionNode, isDirective = false, directiveType?: DirectiveType ): AttributeNode { return { type: 'Attribute', name, value, isDirective, directiveType, range: mockSourceRange }; } // Helper function to create a mock HTML element function createMockHTMLElement( tagName: string, attributes: AttributeNode[] = [], children: any[] = [] ): HTMLElementNode { return { type: 'HTMLElement', tagName, attributes, children, isSelfClosing: false, isVoidElement: false, range: mockSourceRange }; } describe('Basic Dependency Analysis', () => { it('should analyze simple reactive variable dependencies', () => { const countVar = createMockReactiveVariable( 'count', createMockExpression(ExpressionType.LITERAL, undefined, 0) ); const nameVar = createMockReactiveVariable( 'name', createMockExpression(ExpressionType.LITERAL, undefined, 'test') ); const clientBlock: ClientBlockNode = { type: 'ClientBlock', reactiveVariables: [countVar, nameVar], computedValues: [], eventHandlers: [], functions: [], lifecycle: [], range: mockSourceRange, children: [countVar, nameVar] }; const markupBlock: MarkupBlockNode = { type: 'MarkupBlock', elements: [], textNodes: [], interpolations: [], range: mockSourceRange, children: [] }; const component: ComponentNode = { type: 'Component', name: 'TestComponent', props: [], clientBlock, markupBlock, range: mockSourceRange, children: [clientBlock, markupBlock] }; const ast: ComponentAST = { component, dependencies: [], exports: [], sourceMap: { version: 3, sources: [], names: [], mappings: '', sourcesContent: [] } }; const graph = analyzer.analyze(ast); expect(graph.nodes.size).toBe(2); expect(graph.nodes.has('count')).toBe(true); expect(graph.nodes.has('name')).toBe(true); expect(graph.circularDependencies).toHaveLength(0); }); it('should detect dependencies in computed values', () => { const countVar = createMockReactiveVariable( 'count', createMockExpression(ExpressionType.LITERAL, undefined, 0) ); const doubleCountComputed = createMockComputedValue( 'doubleCount', createMockExpression( ExpressionType.BINARY, undefined, undefined, createMockExpression(ExpressionType.IDENTIFIER, 'count'), createMockExpression(ExpressionType.LITERAL, undefined, 2), '*' ) ); const clientBlock: ClientBlockNode = { type: 'ClientBlock', reactiveVariables: [countVar], computedValues: [doubleCountComputed], eventHandlers: [], functions: [], lifecycle: [], range: mockSourceRange, children: [countVar] }; const markupBlock: MarkupBlockNode = { type: 'MarkupBlock', elements: [], textNodes: [], interpolations: [], range: mockSourceRange, children: [] }; const component: ComponentNode = { type: 'Component', name: 'TestComponent', props: [], clientBlock, markupBlock, range: mockSourceRange, children: [clientBlock, markupBlock] }; const ast: ComponentAST = { component, dependencies: [], exports: [], sourceMap: { version: 3, sources: [], names: [], mappings: '', sourcesContent: [] } }; const graph = analyzer.analyze(ast); expect(graph.nodes.size).toBe(2); expect(graph.nodes.has('count')).toBe(true); expect(graph.nodes.has('doubleCount')).toBe(true); const doubleCountNode = graph.nodes.get('doubleCount')!; expect(doubleCountNode.dependencies.has('count')).toBe(true); const countNode = graph.nodes.get('count')!; expect(countNode.dependents.has('doubleCount')).toBe(true); }); it('should analyze interpolation dependencies', () => { const countVar = createMockReactiveVariable( 'count', createMockExpression(ExpressionType.LITERAL, undefined, 0) ); const interpolation = createMockInterpolation( createMockExpression(ExpressionType.IDENTIFIER, 'count') ); const clientBlock: ClientBlockNode = { type: 'ClientBlock', reactiveVariables: [countVar], computedValues: [], eventHandlers: [], functions: [], lifecycle: [], range: mockSourceRange, children: [countVar] }; const markupBlock: MarkupBlockNode = { type: 'MarkupBlock', elements: [], textNodes: [], interpolations: [interpolation], range: mockSourceRange, children: [interpolation] }; const component: ComponentNode = { type: 'Component', name: 'TestComponent', props: [], clientBlock, markupBlock, range: mockSourceRange, children: [clientBlock, markupBlock] }; const ast: ComponentAST = { component, dependencies: [], exports: [], sourceMap: { version: 3, sources: [], names: [], mappings: '', sourcesContent: [] } }; const graph = analyzer.analyze(ast); expect(graph.edges.some(edge => edge.from === 'count' && edge.type === DependencyType.INTERPOLATION )).toBe(true); }); it('should analyze directive dependencies', () => { const countVar = createMockReactiveVariable( 'count', createMockExpression(ExpressionType.LITERAL, undefined, 0) ); const bindAttribute = createMockAttribute( 'bind:value', createMockExpression(ExpressionType.IDENTIFIER, 'count'), true, DirectiveType.BIND ); const element = createMockHTMLElement('input', [bindAttribute]); const clientBlock: ClientBlockNode = { type: 'ClientBlock', reactiveVariables: [countVar], computedValues: [], eventHandlers: [], functions: [], lifecycle: [], range: mockSourceRange, children: [countVar] }; const markupBlock: MarkupBlockNode = { type: 'MarkupBlock', elements: [element], textNodes: [], interpolations: [], range: mockSourceRange, children: [element] }; const component: ComponentNode = { type: 'Component', name: 'TestComponent', props: [], clientBlock, markupBlock, range: mockSourceRange, children: [clientBlock, markupBlock] }; const ast: ComponentAST = { component, dependencies: [], exports: [], sourceMap: { version: 3, sources: [], names: [], mappings: '', sourcesContent: [] } }; const graph = analyzer.analyze(ast); // Bind directive should create both read and write dependencies expect(graph.edges.some(edge => edge.from === 'count' && edge.type === DependencyType.READ )).toBe(true); expect(graph.edges.some(edge => edge.from === 'count' && edge.type === DependencyType.WRITE )).toBe(true); }); }); describe('Circular Dependency Detection', () => { it('should detect simple circular dependencies', () => { // Create variables that depend on each other: a depends on b, b depends on a const aVar = createMockReactiveVariable( 'a', createMockExpression(ExpressionType.IDENTIFIER, 'b') ); const bVar = createMockReactiveVariable( 'b', createMockExpression(ExpressionType.IDENTIFIER, 'a') ); const clientBlock: ClientBlockNode = { type: 'ClientBlock', reactiveVariables: [aVar, bVar], computedValues: [], eventHandlers: [], functions: [], lifecycle: [], range: mockSourceRange, children: [aVar, bVar] }; const markupBlock: MarkupBlockNode = { type: 'MarkupBlock', elements: [], textNodes: [], interpolations: [], range: mockSourceRange, children: [] }; const component: ComponentNode = { type: 'Component', name: 'TestComponent', props: [], clientBlock, markupBlock, range: mockSourceRange, children: [clientBlock, markupBlock] }; const ast: ComponentAST = { component, dependencies: [], exports: [], sourceMap: { version: 3, sources: [], names: [], mappings: '', sourcesContent: [] } }; const graph = analyzer.analyze(ast); expect(graph.circularDependencies.length).toBeGreaterThan(0); expect(graph.nodes.get('a')?.isCircular).toBe(true); expect(graph.nodes.get('b')?.isCircular).toBe(true); expect(analyzer.getErrors().length).toBeGreaterThan(0); }); it('should detect complex circular dependencies', () => { // Create a -> b -> c -> a circular dependency const aVar = createMockReactiveVariable( 'a', createMockExpression(ExpressionType.IDENTIFIER, 'c') ); const bVar = createMockReactiveVariable( 'b', createMockExpression(ExpressionType.IDENTIFIER, 'a') ); const cVar = createMockReactiveVariable( 'c', createMockExpression(ExpressionType.IDENTIFIER, 'b') ); const clientBlock: ClientBlockNode = { type: 'ClientBlock', reactiveVariables: [aVar, bVar, cVar], computedValues: [], eventHandlers: [], functions: [], lifecycle: [], range: mockSourceRange, children: [aVar, bVar, cVar] }; const markupBlock: MarkupBlockNode = { type: 'MarkupBlock', elements: [], textNodes: [], interpolations: [], range: mockSourceRange, children: [] }; const component: ComponentNode = { type: 'Component', name: 'TestComponent', props: [], clientBlock, markupBlock, range: mockSourceRange, children: [clientBlock, markupBlock] }; const ast: ComponentAST = { component, dependencies: [], exports: [], sourceMap: { version: 3, sources: [], names: [], mappings: '', sourcesContent: [] } }; const graph = analyzer.analyze(ast); expect(graph.circularDependencies.length).toBeGreaterThan(0); expect(graph.nodes.get('a')?.isCircular).toBe(true); expect(graph.nodes.get('b')?.isCircular).toBe(true); expect(graph.nodes.get('c')?.isCircular).toBe(true); }); it('should not detect false circular dependencies', () => { // Create a linear dependency chain: a -> b -> c const aVar = createMockReactiveVariable( 'a', createMockExpression(ExpressionType.LITERAL, undefined, 1) ); const bVar = createMockReactiveVariable( 'b', createMockExpression(ExpressionType.IDENTIFIER, 'a') ); const cVar = createMockReactiveVariable( 'c', createMockExpression(ExpressionType.IDENTIFIER, 'b') ); const clientBlock: ClientBlockNode = { type: 'ClientBlock', reactiveVariables: [aVar, bVar, cVar], computedValues: [], eventHandlers: [], functions: [], lifecycle: [], range: mockSourceRange, children: [aVar, bVar, cVar] }; const markupBlock: MarkupBlockNode = { type: 'MarkupBlock', elements: [], textNodes: [], interpolations: [], range: mockSourceRange, children: [] }; const component: ComponentNode = { type: 'Component', name: 'TestComponent', props: [], clientBlock, markupBlock, range: mockSourceRange, children: [clientBlock, markupBlock] }; const ast: ComponentAST = { component, dependencies: [], exports: [], sourceMap: { version: 3, sources: [], names: [], mappings: '', sourcesContent: [] } }; const graph = analyzer.analyze(ast); expect(graph.circularDependencies).toHaveLength(0); expect(graph.nodes.get('a')?.isCircular).toBe(false); expect(graph.nodes.get('b')?.isCircular).toBe(false); expect(graph.nodes.get('c')?.isCircular).toBe(false); }); }); describe('Update Order Calculation', () => { it('should calculate correct update order for linear dependencies', () => { // Create a -> b -> c dependency chain const aVar = createMockReactiveVariable( 'a', createMockExpression(ExpressionType.LITERAL, undefined, 1) ); const bVar = createMockReactiveVariable( 'b', createMockExpression(ExpressionType.IDENTIFIER, 'a') ); const cVar = createMockReactiveVariable( 'c', createMockExpression(ExpressionType.IDENTIFIER, 'b') ); const clientBlock: ClientBlockNode = { type: 'ClientBlock', reactiveVariables: [cVar, aVar, bVar], // Intentionally out of order computedValues: [], eventHandlers: [], functions: [], lifecycle: [], range: mockSourceRange, children: [cVar, aVar, bVar] }; const markupBlock: MarkupBlockNode = { type: 'MarkupBlock', elements: [], textNodes: [], interpolations: [], range: mockSourceRange, children: [] }; const component: ComponentNode = { type: 'Component', name: 'TestComponent', props: [], clientBlock, markupBlock, range: mockSourceRange, children: [clientBlock, markupBlock] }; const ast: ComponentAST = { component, dependencies: [], exports: [], sourceMap: { version: 3, sources: [], names: [], mappings: '', sourcesContent: [] } }; const graph = analyzer.analyze(ast); // Update order should be a, b, c (topological order) expect(graph.updateOrder).toEqual(['a', 'b', 'c']); expect(graph.nodes.get('a')?.updateOrder).toBe(0); expect(graph.nodes.get('b')?.updateOrder).toBe(1); expect(graph.nodes.get('c')?.updateOrder).toBe(2); }); it('should handle independent variables correctly', () => { const aVar = createMockReactiveVariable( 'a', createMockExpression(ExpressionType.LITERAL, undefined, 1) ); const bVar = createMockReactiveVariable( 'b', createMockExpression(ExpressionType.LITERAL, undefined, 2) ); const cVar = createMockReactiveVariable( 'c', createMockExpression(ExpressionType.LITERAL, undefined, 3) ); const clientBlock: ClientBlockNode = { type: 'ClientBlock', reactiveVariables: [aVar, bVar, cVar], computedValues: [], eventHandlers: [], functions: [], lifecycle: [], range: mockSourceRange, children: [aVar, bVar, cVar] }; const markupBlock: MarkupBlockNode = { type: 'MarkupBlock', elements: [], textNodes: [], interpolations: [], range: mockSourceRange, children: [] }; const component: ComponentNode = { type: 'Component', name: 'TestComponent', props: [], clientBlock, markupBlock, range: mockSourceRange, children: [clientBlock, markupBlock] }; const ast: ComponentAST = { component, dependencies: [], exports: [], sourceMap: { version: 3, sources: [], names: [], mappings: '', sourcesContent: [] } }; const graph = analyzer.analyze(ast); // All variables are independent, so they can be in any order expect(graph.updateOrder).toHaveLength(3); expect(graph.updateOrder).toContain('a'); expect(graph.updateOrder).toContain('b'); expect(graph.updateOrder).toContain('c'); }); }); describe('Update Function Generation', () => { it('should generate update functions for reactive variables', () => { const countVar = createMockReactiveVariable( 'count', createMockExpression(ExpressionType.LITERAL, undefined, 0) ); const clientBlock: ClientBlockNode = { type: 'ClientBlock', reactiveVariables: [countVar], computedValues: [], eventHandlers: [], functions: [], lifecycle: [], range: mockSourceRange, children: [countVar] }; const markupBlock: MarkupBlockNode = { type: 'MarkupBlock', elements: [], textNodes: [], interpolations: [], range: mockSourceRange, children: [] }; const component: ComponentNode = { type: 'Component', name: 'TestComponent', props: [], clientBlock, markupBlock, range: mockSourceRange, children: [clientBlock, markupBlock] }; const ast: ComponentAST = { component, dependencies: [], exports: [], sourceMap: { version: 3, sources: [], names: [], mappings: '', sourcesContent: [] } }; const updateFunctions = analyzer.generateUpdateFunctions(ast); expect(updateFunctions).toHaveLength(1); expect(updateFunctions[0].variableName).toBe('count'); expect(updateFunctions[0].updateType).toBe(UpdateType.TEXT_CONTENT); expect(updateFunctions[0].code).toContain('component.state.count'); }); it('should generate different update types based on usage', () => { const countVar = createMockReactiveVariable( 'count', createMockExpression(ExpressionType.LITERAL, undefined, 0) ); const clientBlock: ClientBlockNode = { type: 'ClientBlock', reactiveVariables: [countVar], computedValues: [], eventHandlers: [], functions: [], lifecycle: [], range: mockSourceRange, children: [countVar] }; const markupBlock: MarkupBlockNode = { type: 'MarkupBlock', elements: [], textNodes: [], interpolations: [], range: mockSourceRange, children: [] }; const component: ComponentNode = { type: 'Component', name: 'TestComponent', props: [], clientBlock, markupBlock, range: mockSourceRange, children: [clientBlock, markupBlock] }; const ast: ComponentAST = { component, dependencies: [], exports: [], sourceMap: { version: 3, sources: [], names: [], mappings: '', sourcesContent: [] } }; const updateFunctions = analyzer.generateUpdateFunctions(ast); expect(updateFunctions).toHaveLength(1); expect(updateFunctions[0].code).toBeTruthy(); expect(updateFunctions[0].code.length).toBeGreaterThan(0); }); }); describe('Complex Scenarios', () => { it('should handle mixed dependencies correctly', () => { // Create a complex scenario with multiple types of dependencies const countVar = createMockReactiveVariable( 'count', createMockExpression(ExpressionType.LITERAL, undefined, 0) ); const doubleCountComputed = createMockComputedValue( 'doubleCount', createMockExpression( ExpressionType.BINARY, undefined, undefined, createMockExpression(ExpressionType.IDENTIFIER, 'count'), createMockExpression(ExpressionType.LITERAL, undefined, 2), '*' ) ); const interpolation = createMockInterpolation( createMockExpression(ExpressionType.IDENTIFIER, 'doubleCount') ); const clientBlock: ClientBlockNode = { type: 'ClientBlock', reactiveVariables: [countVar], computedValues: [doubleCountComputed], eventHandlers: [], functions: [], lifecycle: [], range: mockSourceRange, children: [countVar] }; const markupBlock: MarkupBlockNode = { type: 'MarkupBlock', elements: [], textNodes: [], interpolations: [interpolation], range: mockSourceRange, children: [interpolation] }; const component: ComponentNode = { type: 'Component', name: 'TestComponent', props: [], clientBlock, markupBlock, range: mockSourceRange, children: [clientBlock, markupBlock] }; const ast: ComponentAST = { component, dependencies: [], exports: [], sourceMap: { version: 3, sources: [], names: [], mappings: '', sourcesContent: [] } }; const graph = analyzer.analyze(ast); expect(graph.nodes.size).toBe(2); expect(graph.updateOrder).toEqual(['count', 'doubleCount']); expect(graph.edges.some(edge => edge.from === 'count' && edge.type === DependencyType.COMPUTED )).toBe(true); expect(graph.edges.some(edge => edge.from === 'doubleCount' && edge.type === DependencyType.INTERPOLATION )).toBe(true); }); it('should handle nested expressions correctly', () => { const aVar = createMockReactiveVariable( 'a', createMockExpression(ExpressionType.LITERAL, undefined, 1) ); const bVar = createMockReactiveVariable( 'b', createMockExpression(ExpressionType.LITERAL, undefined, 2) ); // Create a complex nested expression: (a + b) * 2 const complexComputed = createMockComputedValue( 'complex', createMockExpression( ExpressionType.BINARY, undefined, undefined, createMockExpression( ExpressionType.BINARY, undefined, undefined, createMockExpression(ExpressionType.IDENTIFIER, 'a'), createMockExpression(ExpressionType.IDENTIFIER, 'b'), '+' ), createMockExpression(ExpressionType.LITERAL, undefined, 2), '*' ) ); const clientBlock: ClientBlockNode = { type: 'ClientBlock', reactiveVariables: [aVar, bVar], computedValues: [complexComputed], eventHandlers: [], functions: [], lifecycle: [], range: mockSourceRange, children: [aVar, bVar] }; const markupBlock: MarkupBlockNode = { type: 'MarkupBlock', elements: [], textNodes: [], interpolations: [], range: mockSourceRange, children: [] }; const component: ComponentNode = { type: 'Component', name: 'TestComponent', props: [], clientBlock, markupBlock, range: mockSourceRange, children: [clientBlock, markupBlock] }; const ast: ComponentAST = { component, dependencies: [], exports: [], sourceMap: { version: 3, sources: [], names: [], mappings: '', sourcesContent: [] } }; const graph = analyzer.analyze(ast); const complexNode = graph.nodes.get('complex')!; expect(complexNode.dependencies.has('a')).toBe(true); expect(complexNode.dependencies.has('b')).toBe(true); const aNode = graph.nodes.get('a')!; const bNode = graph.nodes.get('b')!; expect(aNode.dependents.has('complex')).toBe(true); expect(bNode.dependents.has('complex')).toBe(true); }); }); });