import * as vscode from 'vscode'; import { LinterRule } from './LinterRule'; import { LinterContext, PatternPhase } from '../LinterContext'; export class BlockRule implements LinterRule { public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void { if (!ctx.hasModuleDef) { if (line.startsWith('tpl ')) { ctx.hasModuleDef = true; } else { ctx.addDiag(lineIdx, 0, rawLine.length, "Jede Datei muss mit 'tpl 1.x module ;' beginnen.", vscode.DiagnosticSeverity.Error); ctx.hasModuleDef = true; } } if (line.startsWith('metadata') && ctx.inBlock === 'none') { ctx.inModuleMetadata = true; ctx.hasModuleMetadata = true; ctx.moduleMetadataBlockLine = lineIdx; return; } if (ctx.inModuleMetadata) { this.trackModuleMetadata(line, lineIdx, ctx); return; } if (line.match(/^(pattern|syncmapping|table|definitions?|enumeration|configuration)\b/)) { if (!ctx.hasModuleMetadata && ctx.inBlock === 'none') { ctx.addDiag(lineIdx, 0, line.length, "Ein 'metadata' Block auf Modul-Ebene ist Pflicht, bevor andere Blöcke definiert werden.", vscode.DiagnosticSeverity.Error); } } this.handleBlockOpenings(line, rawLine, lineIdx, ctx); // NEU: if, for und while tracken this.handleControlBlocks(line, rawLine, lineIdx, ctx); if (ctx.inBlock === 'pattern') { this.checkPatternPhases(line, rawLine, lineIdx, ctx, lines); } this.checkEndBlocks(line, rawLine, lineIdx, ctx); } public checkEnd(lines: string[], ctx: LinterContext): void { for (const unclosedLine of ctx.openDefines) { ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Block 'define' wurde nicht mit 'end define;' geschlossen!", vscode.DiagnosticSeverity.Error); } // NEU: Offene if/for/while Blöcke am Dateiende ankreiden for (const block of ctx.openControlBlocks) { ctx.addDiag(block.lineIdx, 0, lines[block.lineIdx].length, `Block '${block.type}' wurde nicht mit 'end ${block.type};' geschlossen!`, vscode.DiagnosticSeverity.Error); } } private handleControlBlocks(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) { // Block wird geöffnet if (/^if\b/.test(line)) { ctx.openControlBlocks.push({ type: 'if', lineIdx }); } else if (/^for\b/.test(line)) { ctx.openControlBlocks.push({ type: 'for', lineIdx }); } else if (/^while\b/.test(line)) { ctx.openControlBlocks.push({ type: 'while', lineIdx }); } // Block wird geschlossen if (line.startsWith('end if')) { this.closeControlBlock('if', line, lineIdx, ctx); } else if (line.startsWith('end for')) { this.closeControlBlock('for', line, lineIdx, ctx); } else if (line.startsWith('end while')) { this.closeControlBlock('while', line, lineIdx, ctx); } } private closeControlBlock(expectedType: string, line: string, lineIdx: number, ctx: LinterContext) { if (ctx.openControlBlocks.length === 0) { ctx.addDiag(lineIdx, 0, line.length, `'end ${expectedType}' gefunden, ohne offenen Block.`, vscode.DiagnosticSeverity.Error); return; } // Wir nehmen den zuletzt geöffneten Block vom Stapel const lastBlock = ctx.openControlBlocks.pop()!; if (lastBlock.type !== expectedType) { ctx.addDiag(lineIdx, 0, line.length, `Falsches Ende. Erwartet: 'end ${lastBlock.type};', gefunden: 'end ${expectedType};'.`, vscode.DiagnosticSeverity.Error); // Damit Folgefehler vermieden werden, legen wir ihn zurück ctx.openControlBlocks.push(lastBlock); } } private trackModuleMetadata(line: string, lineIdx: number, ctx: LinterContext) { if (line.match(/^origin\b/)) { ctx.foundModuleMetadata.origin = true; } if (line.match(/^author\b/)) { ctx.foundModuleMetadata.author = true; } if (line.match(/^products\b/)) { ctx.foundModuleMetadata.products = true; } if (line.match(/^publishers\b/)) { ctx.foundModuleMetadata.publishers = true; } if (line.match(/^tree_path\b/)) { ctx.foundModuleMetadata.tree_path = true; } if (line.startsWith('end metadata')) { ctx.inModuleMetadata = false; const missing = Object.keys(ctx.foundModuleMetadata).filter(k => !(ctx.foundModuleMetadata as any)[k]); if (missing.length > 0 && ctx.moduleMetadataBlockLine !== -1) { const diag = ctx.addDiag(ctx.moduleMetadataBlockLine, 0, 8, `Fehlende Pflicht-Metadaten (Modul-Ebene): ${missing.join(', ')}`, vscode.DiagnosticSeverity.Error); diag.code = 'missing_module_metadata'; } } } private handleBlockOpenings(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) { if (line.startsWith('definitions ') || line.startsWith('definition ')) { if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "Ein Definitions-Block muss auf oberster Ebene deklariert werden.", vscode.DiagnosticSeverity.Error); } ctx.inBlock = 'definitions'; } else if (line.startsWith('define ')) { ctx.openDefines.push(lineIdx); } else if (line.startsWith('enumeration ')) { if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "Eine Enumeration muss auf oberster Ebene deklariert werden.", vscode.DiagnosticSeverity.Error); } ctx.inBlock = 'enumeration'; } else if (line.startsWith('configuration ')) { if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "Eine Konfiguration muss auf oberster Ebene deklariert werden.", vscode.DiagnosticSeverity.Error); } ctx.inBlock = 'configuration'; } else if (line.startsWith('pattern ') || line.startsWith('syncmapping ')) { if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, 7, "Ein Pattern/Syncmapping kann nicht innerhalb eines anderen Blocks deklariert werden.", vscode.DiagnosticSeverity.Error); } ctx.inBlock = 'pattern'; ctx.currentPatternPhase = PatternPhase.None; ctx.activePhaseName = ""; ctx.activePhaseLine = -1; const nameMatch = line.match(/(pattern|syncmapping)\s+([a-zA-Z0-9_]+)\s+\d+\.\d+/); if (nameMatch) { ctx.currentPatternName = nameMatch[2]; } else { ctx.addDiag(lineIdx, 0, rawLine.length, "Ungültige Pattern-Deklaration. Erwartet: 'pattern '", vscode.DiagnosticSeverity.Error); } } else if (line.startsWith('table ')) { if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, 5, "Eine Tabelle kann nicht innerhalb eines anderen Blocks deklariert werden.", vscode.DiagnosticSeverity.Error); } ctx.inBlock = 'table'; } } private checkPatternPhases(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]) { const checkPhase = (phaseName: string, phaseEnum: PatternPhase) => { if (line.startsWith(phaseName)) { if (ctx.activePhaseName !== "") { ctx.addDiag(ctx.activePhaseLine, 0, ctx.activePhaseName.length, `Block '${ctx.activePhaseName}' wurde nicht mit 'end ${ctx.activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); } if (ctx.currentPatternPhase >= phaseEnum) { ctx.addDiag(lineIdx, 0, phaseName.length, `'${phaseName}' Block ist hier nicht erlaubt.`, vscode.DiagnosticSeverity.Error); } ctx.currentPatternPhase = phaseEnum; ctx.activePhaseName = phaseName; ctx.activePhaseLine = lineIdx; } }; checkPhase('metadata', PatternPhase.Metadata); checkPhase('overview', PatternPhase.Overview); checkPhase('constants', PatternPhase.Constants); checkPhase('triggers', PatternPhase.Triggers); checkPhase('mapping', PatternPhase.Mapping); checkPhase('removal', PatternPhase.Removal); if (line.startsWith('body')) { if (ctx.activePhaseName !== "") { ctx.addDiag(ctx.activePhaseLine, 0, ctx.activePhaseName.length, `Block '${ctx.activePhaseName}' wurde nicht mit 'end ${ctx.activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); } ctx.activePhaseName = "body"; ctx.activePhaseLine = lineIdx; let foundLog = false; for (let j = lineIdx + 1; j < lines.length; j++) { const aheadLine = lines[j].trim(); if (aheadLine.startsWith('end pattern') || aheadLine.startsWith('end syncmapping')) { break; } if (aheadLine.includes('log.debug') && aheadLine.includes(ctx.currentPatternName)) { foundLog = true; break; } } if (!foundLog) { const expectedLog = `log.debug("Start pattern ${ctx.currentPatternName}");`; const diag = ctx.addDiag(lineIdx, 0, rawLine.length, `Im Body fehlt das Log-Statement mit dem Pattern-Namen. Bsp: ${expectedLog}`, vscode.DiagnosticSeverity.Error); diag.code = 'missing_log_start'; diag.source = expectedLog; } } const closers = ['end metadata', 'end overview', 'end constants', 'end triggers', 'end mapping', 'end removal', 'end body']; for (const closer of closers) { if (line.startsWith(closer)) { const phase = closer.replace('end ', '').replace(';', '').trim(); if (ctx.activePhaseName !== phase) { ctx.addDiag(lineIdx, 0, line.length, `Unerwartetes '${closer}'. Erwartet wurde 'end ${ctx.activePhaseName};'.`, vscode.DiagnosticSeverity.Error); } else { ctx.activePhaseName = ""; } } } } private checkEndBlocks(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) { if (line.startsWith('end pattern') || line.startsWith('end syncmapping')) { if (ctx.activePhaseName !== "") { ctx.addDiag(ctx.activePhaseLine, 0, ctx.activePhaseName.length, `Block '${ctx.activePhaseName}' wurde nicht mit 'end ${ctx.activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); } if (ctx.inBlock !== 'pattern') { ctx.addDiag(lineIdx, 0, 11, "'end pattern' gefunden, ohne offenen Pattern-Block.", vscode.DiagnosticSeverity.Error); } ctx.inBlock = 'none'; } else if (line.startsWith('end table') || line.startsWith('end enumeration') || line.startsWith('end configuration')) { ctx.inBlock = 'none'; } else if (line.startsWith('end definitions') || line.startsWith('end definition')) { if (ctx.inBlock !== 'definitions') { ctx.addDiag(lineIdx, 0, line.length, "'end definitions' gefunden, ohne offenen Definitions-Block.", vscode.DiagnosticSeverity.Error); } ctx.inBlock = 'none'; } else if (line.startsWith('end define')) { if (ctx.openDefines.length > 0) { ctx.openDefines.pop(); } else { ctx.addDiag(lineIdx, 0, line.length, "'end define' gefunden, ohne offenes 'define'.", vscode.DiagnosticSeverity.Error); } } } }