improve control flow transformer robustness and remove debug logs

This commit is contained in:
Michał Sieciechowicz 2026-01-18 20:27:12 +01:00
parent 990aef92ef
commit c3dda73d3a
8 changed files with 287 additions and 69 deletions

View File

@ -56,10 +56,20 @@ export class ControlFlowTransformer {
closeParenIndex--;
const openBraceIndex = content.indexOf('{', closeParenIndex);
if (openBraceIndex === -1) return null;
if (openBraceIndex === -1) {
// Handle case where there's no opening brace - create a simple block
return {
match: content.substring(ifIndex, closeParenIndex + 1) + '{ }',
startIndex: ifIndex,
endIndex: closeParenIndex + 1
};
}
let endIndex = this.findIfBlockEnd(content, openBraceIndex);
if (endIndex === -1) return null;
if (endIndex === -1) {
// For incomplete blocks, try to process what we have
endIndex = content.length;
}
return {
match: content.substring(ifIndex, endIndex),
@ -79,7 +89,21 @@ export class ControlFlowTransformer {
index++;
}
if (braceCount !== 0) return -1;
// If we couldn't find matching closing brace, try to handle incomplete blocks
if (braceCount !== 0) {
// For incomplete blocks, find the end of the current line or next @if/@for statement
const remainingContent = content.substring(startBraceIndex);
const nextIfIndex = remainingContent.indexOf('@if', 1);
const nextForIndex = remainingContent.indexOf('@for', 1);
const lineEndIndex = remainingContent.indexOf('\n');
let endIndex = content.length;
if (nextIfIndex !== -1) endIndex = Math.min(endIndex, startBraceIndex + nextIfIndex);
if (nextForIndex !== -1) endIndex = Math.min(endIndex, startBraceIndex + nextForIndex);
if (lineEndIndex !== -1) endIndex = Math.min(endIndex, startBraceIndex + lineEndIndex);
return endIndex;
}
while (index < content.length) {
const remaining = content.substring(index);
@ -165,7 +189,21 @@ export class ControlFlowTransformer {
if (forIndex === -1) return null;
const openParenIndex = content.indexOf('(', forIndex);
const closeParenIndex = content.indexOf(')', openParenIndex);
if (openParenIndex === -1) return null;
// Properly count nested parentheses
let parenCount = 1;
let closeParenIndex = openParenIndex + 1;
while (closeParenIndex < content.length && parenCount > 0) {
const char = content[closeParenIndex];
if (char === '(') parenCount++;
else if (char === ')') parenCount--;
closeParenIndex++;
}
if (parenCount !== 0) return null;
closeParenIndex--; // Move back to the closing paren
const openBraceIndex = content.indexOf('{', closeParenIndex);
if (openBraceIndex === -1) return null;
@ -194,7 +232,21 @@ export class ControlFlowTransformer {
if (startIndex === -1) return null;
const openParenIndex = match.indexOf('(', startIndex);
const closeParenIndex = match.indexOf(')', openParenIndex);
if (openParenIndex === -1) return null;
// Properly count nested parentheses
let parenCount = 1;
let closeParenIndex = openParenIndex + 1;
while (closeParenIndex < match.length && parenCount > 0) {
const char = match[closeParenIndex];
if (char === '(') parenCount++;
else if (char === ')') parenCount--;
closeParenIndex++;
}
if (parenCount !== 0) return null;
closeParenIndex--; // Move back to the closing paren
const openBraceIndex = match.indexOf('{', closeParenIndex);
if (openBraceIndex === -1) return null;
@ -219,7 +271,8 @@ export class ControlFlowTransformer {
const forPart = parts[0].trim();
const trackPart = parts[1]?.trim();
const forMatch = forPart.match(/^\s*([^\s]+)\s+of\s+([^\s]+)\s*$/);
// Match: variable of iterable (iterable can contain parentheses, dots, etc.)
const forMatch = forPart.match(/^\s*(\w+)\s+of\s+(.+)\s*$/);
if (!forMatch) return null;
const variable = forMatch[1].trim();
@ -248,7 +301,10 @@ export class ControlFlowTransformer {
ngForExpression += `; trackBy: ${forBlock.trackBy}`;
}
return `<ng-container *ngFor="${ngForExpression}">${forBlock.content}</ng-container>`;
// Recursively transform nested @if and @for blocks in content
const transformedContent = this.transform(forBlock.content);
return `<ng-container *ngFor="${ngForExpression}">${transformedContent}</ng-container>`;
}
private parseBlocks(match: string): ControlFlowBlock[] {
@ -273,6 +329,12 @@ export class ControlFlowTransformer {
const { condition, aliasVariable } = this.parseConditionWithAlias(conditionStr.trim());
const openBraceIndex = match.indexOf('{', closeParenIndex);
if (openBraceIndex === -1) {
// Incomplete @if block - no opening brace found
blocks.push({ condition, content: '', aliasVariable });
return blocks;
}
let braceCount = 1;
let closeBraceIndex = openBraceIndex + 1;
@ -282,11 +344,22 @@ export class ControlFlowTransformer {
else if (char === '}') braceCount--;
closeBraceIndex++;
}
closeBraceIndex--;
const content = match.substring(openBraceIndex + 1, closeBraceIndex);
let content = '';
if (braceCount === 0) {
// Complete block found
closeBraceIndex--;
content = match.substring(openBraceIndex + 1, closeBraceIndex);
index = closeBraceIndex + 1;
} else {
// Incomplete block - take everything after opening brace
content = match.substring(openBraceIndex + 1);
index = match.length;
}
// Trim leading whitespace from content
content = content.trimStart();
blocks.push({ condition, content, aliasVariable });
index = closeBraceIndex + 1;
}
while (index < match.length) {
@ -311,6 +384,13 @@ export class ControlFlowTransformer {
const { condition, aliasVariable } = this.parseConditionWithAlias(conditionStr.trim());
const openBraceIndex = match.indexOf('{', closeParenIndex);
if (openBraceIndex === -1) {
// Incomplete @else if block - no opening brace found
blocks.push({ condition, content: '', aliasVariable });
index = match.length;
break;
}
let braceCount = 1;
let closeBraceIndex = openBraceIndex + 1;
@ -320,11 +400,22 @@ export class ControlFlowTransformer {
else if (char === '}') braceCount--;
closeBraceIndex++;
}
closeBraceIndex--;
const content = match.substring(openBraceIndex + 1, closeBraceIndex);
let content = '';
if (braceCount === 0) {
// Complete block found
closeBraceIndex--;
content = match.substring(openBraceIndex + 1, closeBraceIndex);
index = closeBraceIndex + 1;
} else {
// Incomplete block - take everything after opening brace
content = match.substring(openBraceIndex + 1);
index = match.length;
}
// Trim leading whitespace from content
content = content.trimStart();
blocks.push({ condition, content, aliasVariable });
index = closeBraceIndex + 1;
} else if (elseMatch) {
const openBraceIndex = index + elseMatch[0].length - 1;
let braceCount = 1;
@ -336,11 +427,22 @@ export class ControlFlowTransformer {
else if (char === '}') braceCount--;
closeBraceIndex++;
}
closeBraceIndex--;
const content = match.substring(openBraceIndex + 1, closeBraceIndex);
let content = '';
if (braceCount === 0) {
// Complete block found
closeBraceIndex--;
content = match.substring(openBraceIndex + 1, closeBraceIndex);
index = closeBraceIndex + 1;
} else {
// Incomplete block - take everything after opening brace
content = match.substring(openBraceIndex + 1);
index = match.length;
}
// Trim leading whitespace from content
content = content.trimStart();
blocks.push({ condition: null, content });
index = closeBraceIndex + 1;
} else {
break;
}
@ -368,10 +470,13 @@ export class ControlFlowTransformer {
const block = blocks[i];
const condition = this.buildCondition(block.condition, negated);
// Recursively transform nested @if and @for blocks in content
const transformedContent = this.transform(block.content);
if (block.aliasVariable) {
result += `<ng-container *ngIf="${condition}; let ${block.aliasVariable}">${block.content}</ng-container>`;
result += `<ng-container *ngIf="${condition}; let ${block.aliasVariable}">${transformedContent}</ng-container>`;
} else {
result += `<ng-container *ngIf="${condition}">${block.content}</ng-container>`;
result += `<ng-container *ngIf="${condition}">${transformedContent}</ng-container>`;
}
if (i < blocks.length - 1) {

View File

@ -160,6 +160,7 @@ class Server extends BaseBuilder {
}
private proxyRequest(targetUrl: string, req: http.IncomingMessage, res: http.ServerResponse): void {
console.log(`[Proxy] ${req.method} ${req.url} -> ${targetUrl}`);
const parsedUrl = new URL(targetUrl);
const protocol = parsedUrl.protocol === 'https:' ? https : http;
@ -173,13 +174,14 @@ class Server extends BaseBuilder {
},
},
(proxyRes) => {
console.log(`[Proxy] Response: ${proxyRes.statusCode} for ${req.url}`);
res.writeHead(proxyRes.statusCode || 500, proxyRes.headers);
proxyRes.pipe(res);
},
);
proxyReq.on('error', (err) => {
console.error('Proxy error:', err.message);
console.error(`[Proxy] Error for ${req.url}:`, err.message);
res.writeHead(502);
res.end('Bad Gateway');
});

View File

@ -61,27 +61,11 @@ export class Injector {
}
public createInstanceWithProviders<T>(classType: Type<T>, providers: Provider[]): T {
console.log('[DI] createInstanceWithProviders START', {
classType,
typeofClassType: typeof classType,
isFunction: typeof classType === 'function',
hasName: classType?.name,
originalName: (classType as any)?.__quarc_original_name__,
providers: providers.map(p => ({
provide: typeof p.provide === 'string' ? p.provide : p.provide?.name,
type: 'useValue' in p ? 'useValue' : 'useClass' in p ? 'useClass' : 'useFactory' in p ? 'useFactory' : 'useExisting'
}))
});
if (!classType) {
throw new Error(`[DI] createInstanceWithProviders called with undefined classType`);
}
try {
console.log({
className: (classType as any).__quarc_original_name__ || classType.name,
classType,
});
const dependencies = this.resolveDependenciesWithProviders(classType, providers);
/** /
console.log({
@ -127,11 +111,6 @@ export class Injector {
return `${metadata.selector} (class)`;
}
console.log({
classType,
metadata,
});
return 'Unknown class';
}
@ -187,44 +166,27 @@ export class Injector {
}
private resolveDependency(token: any, providers: Provider[]): any {
console.log('[DI] resolveDependency', {
token,
typeofToken: typeof token,
tokenName: typeof token === 'string' ? token : (token as any).__quarc_original_name__ || token?.name,
isFunction: typeof token === 'function',
});
const tokenName = typeof token === 'string' ? token : (token as any).__quarc_original_name__ || token.name;
const provider = this.findProvider(token, providers);
if (provider) {
console.log('[DI] Found provider for token', tokenName);
return this.resolveProviderValue(provider, providers);
}
if (this.sharedInstances[tokenName]) {
console.log('[DI] Found in sharedInstances', tokenName);
return this.sharedInstances[tokenName];
}
if (this.instanceCache[tokenName]) {
console.log('[DI] Found in instanceCache', tokenName);
return this.instanceCache[tokenName];
}
console.log('[DI] Creating new instance for token', tokenName);
return this.createInstanceWithProviders(token, providers);
}
private getConstructorParameterTypes(classType: Type<any>): any[] {
const className = classType?.name || 'Unknown';
console.log({
className,
classType,
diParams: (classType as any).__di_params__,
});
if (!classType) {
throw new Error(`[DI] Cannot resolve dependencies: classType is undefined`);
}
@ -256,12 +218,10 @@ export class Injector {
public register<T>(classType: Type<T>, instance: T | Type<T>): void {
const key = (classType as any).__quarc_original_name__ || classType.name;
this.instanceCache[key] = instance;
console.log('injector register', classType, key, instance);
}
public registerShared<T>(classType: Type<T>, instance: T | Type<T>): void {
const key = (classType as any).__quarc_original_name__ || classType.name;
console.log('injector registerShared', classType, key, instance);
this.sharedInstances[key] = instance;
}

View File

@ -235,7 +235,6 @@ export class TemplateFragment {
const isForIn = !!forInMatch;
if (!match) {
console.warn('Invalid ngFor expression:', ngForExpression);
parent.insertBefore(endMarker, ngContainer);
ngContainer.remove();
return;
@ -376,7 +375,6 @@ export class TemplateFragment {
*/
rerenderFragment(markerIndex: number): void {
if (markerIndex < 0 || markerIndex >= this.ngContainerMarkers.length) {
console.warn('Invalid marker index:', markerIndex);
return;
}

View File

@ -24,7 +24,6 @@ export class WebComponentFactory {
const componentMeta = componentType._quarcComponent?.[0];
if (!componentMeta) {
console.warn(`Component ${componentType.name} has no _quarcComponent metadata`);
return false;
}
@ -69,7 +68,6 @@ export class WebComponentFactory {
this.componentTypes.set(tagName, componentType);
return true;
} catch (error) {
console.warn(`Failed to register component ${tagName}:`, error);
return false;
}
}
@ -108,7 +106,6 @@ export class WebComponentFactory {
}
}
console.log({localProviders});
return injector.createInstanceWithProviders<IComponent>(componentType, localProviders);
}

View File

@ -35,11 +35,8 @@ async function tryLoadExternalScripts(urls: string | string[]): Promise<void> {
await loadExternalScript(url);
return;
} catch {
console.warn(`[External] Could not load from: ${url}`);
}
}
console.info("[External] No external scripts loaded - app continues without enhancements");
}
export async function bootstrapApplication(

View File

@ -15,7 +15,6 @@ export class RouterLink implements IDirective {
public _nativeElement: HTMLElement,
private activatedRoute?: ActivatedRoute,
) {
console.log({ routerLink: this.routerLink() });
this._nativeElement.addEventListener('click', (event) => {
this.onClick(event);
});
@ -44,7 +43,6 @@ export class RouterLink implements IDirective {
this.router.navigate(commands, extras).then(success => {
}).catch(error => {
console.error('RouterLink CLICK - Navigation failed:', error);
});
}

View File

@ -261,6 +261,167 @@ test('ControlFlowTransformer: @if @else if oba z aliasem', () => {
result.includes('*ngIf="!(primary()) && !(secondary())"');
});
// Test 25: ControlFlowTransformer - niekompletny @if (bez zamknięcia)
test('ControlFlowTransformer: niekompletny @if', () => {
const transformer = new ControlFlowTransformer();
const input = '@if (range.name) { content';
const result = transformer.transform(input);
return result.includes('<ng-container *ngIf="range.name">content</ng-container>');
});
// Test 26: ControlFlowTransformer - niekompletny @if na końcu
test('ControlFlowTransformer: niekompletny @if na końcu', () => {
const transformer = new ControlFlowTransformer();
const input = '} @if (prepared.sensor.loading) { ';
const result = transformer.transform(input);
return result.includes('<ng-container *ngIf="prepared.sensor.loading"></ng-container>');
});
// Test 27: ControlFlowTransformer - niekompletny @if bez nawiasu klamrowego
test('ControlFlowTransformer: niekompletny @if bez nawiasu klamrowego', () => {
const transformer = new ControlFlowTransformer();
const input = '@if (condition) ';
const result = transformer.transform(input);
return result.includes('<ng-container *ngIf="condition"></ng-container>');
});
// Test 28: ControlFlowTransformer - zagnieżdżony @if wewnątrz @for
test('ControlFlowTransformer: zagnieżdżony @if wewnątrz @for', () => {
const transformer = new ControlFlowTransformer();
const input = `@for (item of items) {
@if (item.active) {
<span>{{ item.name }}</span>
}
}`;
const result = transformer.transform(input);
return result.includes('<ng-container *ngFor="let item of items">') &&
result.includes('<ng-container *ngIf="item.active">') &&
!result.includes('@if') &&
!result.includes('@for');
});
// Test 29: ControlFlowTransformer - zagnieżdżony @for wewnątrz @if
test('ControlFlowTransformer: zagnieżdżony @for wewnątrz @if', () => {
const transformer = new ControlFlowTransformer();
const input = `@if (hasItems) {
@for (item of items; track item.id) {
<div>{{ item }}</div>
}
}`;
const result = transformer.transform(input);
return result.includes('<ng-container *ngIf="hasItems">') &&
result.includes('<ng-container *ngFor="let item of items; trackBy: item.id">') &&
!result.includes('@if') &&
!result.includes('@for');
});
// Test 30: ControlFlowTransformer - wielokrotnie zagnieżdżone @if wewnątrz @for
test('ControlFlowTransformer: wielokrotnie zagnieżdżone @if wewnątrz @for', () => {
const transformer = new ControlFlowTransformer();
const input = `@for (range of ranges; track $index) {
<div>
@if (range.name) {
<span>{{ range.name }}</span>
}
<span>{{ range.min }}</span>
</div>
}`;
const result = transformer.transform(input);
return result.includes('<ng-container *ngFor="let range of ranges; trackBy: $index">') &&
result.includes('<ng-container *ngIf="range.name">') &&
!result.includes('@if') &&
!result.includes('@for');
});
// Test 31: ControlFlowTransformer - kompleksowy przypadek z user template
test('ControlFlowTransformer: kompleksowy przypadek użytkownika', () => {
const transformer = new ControlFlowTransformer();
const input = `@if (prepared.sensor.ranges) {
<span>
@for (range of prepared.sensor.ranges; track $index) {
<div>
@if (range.name) {
<span>{{ range.name }}</span>
}
<span>{{ range.min }}</span>
<span>{{ range.max }}</span>
</div>
}
</span>
}`;
const result = transformer.transform(input);
return result.includes('<ng-container *ngIf="prepared.sensor.ranges">') &&
result.includes('<ng-container *ngFor="let range of prepared.sensor.ranges; trackBy: $index">') &&
result.includes('<ng-container *ngIf="range.name">') &&
!result.includes('@if') &&
!result.includes('@for');
});
// Test 32: ControlFlowTransformer - głęboko zagnieżdżone @if/@for/@if
test('ControlFlowTransformer: głęboko zagnieżdżone @if/@for/@if', () => {
const transformer = new ControlFlowTransformer();
const input = `@if (level1) {
@for (item of items; track item.id) {
@if (item.visible) {
<div>{{ item.name }}</div>
}
}
}`;
const result = transformer.transform(input);
return result.includes('<ng-container *ngIf="level1">') &&
result.includes('<ng-container *ngFor="let item of items; trackBy: item.id">') &&
result.includes('<ng-container *ngIf="item.visible">') &&
!result.includes('@if') &&
!result.includes('@for');
});
// Test 33: ControlFlowTransformer - @for z funkcją w iterable
test('ControlFlowTransformer: @for z funkcją w iterable', () => {
const transformer = new ControlFlowTransformer();
const input = '@for (item of getItems(); track item.id) { <div>{{ item }}</div> }';
const result = transformer.transform(input);
return result.includes('<ng-container *ngFor="let item of getItems(); trackBy: item.id">') &&
!result.includes('@for');
});
// Test 34: ControlFlowTransformer - @for z zagnieżdżonymi nawiasami w funkcji
test('ControlFlowTransformer: @for z zagnieżdżonymi nawiasami w funkcji', () => {
const transformer = new ControlFlowTransformer();
const input = '@for (prepared of preparedSensors(); track prepared.sensor.id) { <div>{{ prepared.sensor.name }}</div> }';
const result = transformer.transform(input);
return result.includes('<ng-container *ngFor="let prepared of preparedSensors(); trackBy: prepared.sensor.id">') &&
result.includes('prepared.sensor.name') &&
!result.includes('@for');
});
// Test 35: ControlFlowTransformer - @for z metodą obiektu w iterable
test('ControlFlowTransformer: @for z metodą obiektu w iterable', () => {
const transformer = new ControlFlowTransformer();
const input = '@for (item of data.getItems(); track $index) { <span>{{ item }}</span> }';
const result = transformer.transform(input);
return result.includes('<ng-container *ngFor="let item of data.getItems(); trackBy: $index">') &&
!result.includes('@for');
});
// Test 36: ControlFlowTransformer - @for z wieloma zagnieżdżonymi nawiasami
test('ControlFlowTransformer: @for z wieloma zagnieżdżonymi nawiasami', () => {
const transformer = new ControlFlowTransformer();
const input = '@for (item of service.getData(filter(value())); track item.id) { <div>{{ item }}</div> }';
const result = transformer.transform(input);
return result.includes('<ng-container *ngFor="let item of service.getData(filter(value())); trackBy: item.id">') &&
!result.includes('@for');
});
// Test 37: ControlFlowTransformer - @for z funkcją i złożonym trackBy
test('ControlFlowTransformer: @for z funkcją i złożonym trackBy', () => {
const transformer = new ControlFlowTransformer();
const input = '@for (range of prepared.sensor.ranges; track $index) { <div>{{ range.name }}</div> }';
const result = transformer.transform(input);
return result.includes('<ng-container *ngFor="let range of prepared.sensor.ranges; trackBy: $index">') &&
result.includes('range.name') &&
!result.includes('@for');
});
console.log('\n=== PODSUMOWANIE ===');
console.log(`✅ Testy zaliczone: ${passedTests}`);
console.log(`❌ Testy niezaliczone: ${failedTests}`);