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