import * as vscode from 'vscode'; import { TplCodeActionProvider } from './TplCodeActionProvider'; import { TplDocumentFormatter } from './TplDocumentFormatter'; // --- Konstanten --- const SUPPORTED_TPL_VERSION = 23; // TPL 1.23 const warnedDocuments = new Set(); // Erlaubte Phasen in einem Pattern (strikte Reihenfolge laut BMC TPL) enum PatternPhase { None = 0, Metadata = 1, Overview = 2, Constants = 3, Triggers = 4, Body = 5 } export function activate(context: vscode.ExtensionContext) { // 1. Diagnostic Collection (Der Scanner für rote/gelbe Wellenlinien) const diagnosticCollection = vscode.languages.createDiagnosticCollection('tpl-scanner'); context.subscriptions.push(diagnosticCollection); // 2. Events registrieren (Ausführen beim Öffnen und Ändern) vscode.workspace.onDidOpenTextDocument(doc => validateTplDocument(doc, diagnosticCollection)); vscode.workspace.onDidChangeTextDocument(event => validateTplDocument(event.document, diagnosticCollection)); // 3. Quick-Fix Provider (Code Actions für Glühbirnen-Menü) registrieren context.subscriptions.push( vscode.languages.registerCodeActionsProvider('tpl', new TplCodeActionProvider(), { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] }) ); // 4. Format Document Provider (Auto-Aufräumen mit Shift+Alt+F) context.subscriptions.push( vscode.languages.registerDocumentFormattingEditProvider('tpl', new TplDocumentFormatter()) ); } function validateTplDocument(document: vscode.TextDocument, collection: vscode.DiagnosticCollection) { if (document.languageId !== 'tpl') { return; } const diagnostics: vscode.Diagnostic[] = []; const text = document.getText(); const lines = text.split('\n'); let tplVersion = 10; let toleranceMode = false; let hasModuleDef = false; // --- State Machine Variablen --- let inBlock: 'none' | 'pattern' | 'table' | 'definitions' = 'none'; let currentPatternPhase: PatternPhase = PatternPhase.None; let inMultiLineComment = false; // State Tracking für Quick-Fixes let currentPatternName = ""; let foundMetadata = { author: false, publisher: false, origin: false, tree_path: false }; let metadataBlockLine = -1; let bodyLineCount = 0; // --- 1. Versions-Check im Header --- const versionMatch = text.match(/tpl\s+1\.(\d+)\s+module/); if (versionMatch) { tplVersion = parseInt(versionMatch[1], 10); if (tplVersion > SUPPORTED_TPL_VERSION) { toleranceMode = true; if (!warnedDocuments.has(document.uri.toString())) { vscode.window.showInformationMessage(`TPL 1.${tplVersion} erkannt. Linter läuft im Toleranz-Modus (Fehler werden zu Warnungen).`); warnedDocuments.add(document.uri.toString()); } } } function addDiag(lineIdx: number, start: number, end: number, msg: string, severity: vscode.DiagnosticSeverity): vscode.Diagnostic { if (toleranceMode && severity === vscode.DiagnosticSeverity.Error) { severity = vscode.DiagnosticSeverity.Warning; } const diag = new vscode.Diagnostic(new vscode.Range(lineIdx, start, lineIdx, end), msg, severity); diagnostics.push(diag); return diag; } // --- 2. Der Haupt-Scanner (Zeile für Zeile) --- for (let i = 0; i < lines.length; i++) { const rawLine = lines[i]; const line = rawLine.trim(); // --- Tab-Check (Strikte 3-Spaces-Regel) --- const tabIndex = rawLine.indexOf('\t'); if (tabIndex !== -1) { const diag = addDiag( i, tabIndex, rawLine.length, "Tabs sind hier nicht erlaubt! Bitte nutze exakt 3 Spaces für Einrückungen. (Tipp: Mit 'Alt + Shift' + Mausklick/Pfeiltasten kannst du mehrere Zeilen gleichzeitig markieren und korrigieren).", vscode.DiagnosticSeverity.Warning ); diag.code = 'invalid_tab'; } // Kommentare überspringen (aber Regex für Semikolon-Check braucht MultiLine-Info) if (line.startsWith('//')) { continue; } if (line.startsWith('/*')) { inMultiLineComment = true; continue; } if (inMultiLineComment) { if (line.includes('*/')) { inMultiLineComment = false; } continue; } if (line.length === 0) { continue; } // --- Semikolon-Check --- const noSemicolonNeeded = /^(pattern|table|metadata|overview|constants|triggers|body|end\s+|if\s+|else|elif\s+|for\s+|while\s+|\/\/|\/\*|\*)/; if (!noSemicolonNeeded.test(line)) { if (!line.endsWith(';') && !line.endsWith('{') && !line.endsWith(',')) { addDiag(i, rawLine.length - 1 < 0 ? 0 : rawLine.length - 1, rawLine.length, "Fehlendes Semikolon am Ende der Anweisung.", vscode.DiagnosticSeverity.Error); } } // Modul-Definition prüfen (Muss als Erstes stehen) if (!hasModuleDef) { if (line.startsWith('tpl ')) { hasModuleDef = true; } else { addDiag(i, 0, rawLine.length, "Jede Datei muss mit 'tpl 1.x module ;' beginnen.", vscode.DiagnosticSeverity.Error); hasModuleDef = true; } } // --- Block-Einstiege (Pattern & Table) --- if (line.startsWith('pattern ')) { if ((inBlock as string) !== 'none') { addDiag(i, 0, 7, "Ein Pattern kann nicht innerhalb eines anderen Blocks deklariert werden.", vscode.DiagnosticSeverity.Error); } inBlock = 'pattern'; currentPatternPhase = PatternPhase.None; const nameMatch = line.match(/pattern\s+([a-zA-Z0-9_]+)\s+\d+\.\d+/); if (nameMatch) { currentPatternName = nameMatch[1]; } else { addDiag(i, 0, rawLine.length, "Ungültige Pattern-Deklaration. Erwartet: 'pattern '", vscode.DiagnosticSeverity.Error); } foundMetadata = { author: false, publisher: false, origin: false, tree_path: false }; metadataBlockLine = -1; bodyLineCount = 0; continue; } if (line.startsWith('table ')) { if ((inBlock as string) !== 'none') { addDiag(i, 0, 5, "Eine Tabelle kann nicht innerhalb eines anderen Blocks deklariert werden.", vscode.DiagnosticSeverity.Error); } inBlock = 'table'; if (!line.match(/table\s+[a-zA-Z0-9_]+\s+\d+\.\d+/)) { addDiag(i, 0, rawLine.length, "Ungültige Tabellen-Deklaration. Erwartet: 'table '", vscode.DiagnosticSeverity.Error); } continue; } // --- Innerhalb eines Patterns --- if ((inBlock as string) === 'pattern') { const checkPhase = (phaseName: string, phaseEnum: PatternPhase) => { if (line.startsWith(phaseName)) { if ((currentPatternPhase as PatternPhase) >= phaseEnum) { addDiag(i, 0, phaseName.length, `'${phaseName}' Block ist hier nicht erlaubt. Die Reihenfolge ist: metadata -> overview -> constants -> triggers -> body.`, vscode.DiagnosticSeverity.Error); } if ((currentPatternPhase as PatternPhase) === PatternPhase.Metadata && phaseEnum > PatternPhase.Metadata) { const missing = Object.keys(foundMetadata).filter(k => !(foundMetadata as any)[k]); if (missing.length > 0 && metadataBlockLine !== -1) { const diag = addDiag(metadataBlockLine, 0, 8, `Fehlende Pflicht-Metadaten: ${missing.join(', ')}`, vscode.DiagnosticSeverity.Error); diag.code = 'missing_metadata'; } } currentPatternPhase = phaseEnum; if (phaseEnum === PatternPhase.Metadata) { metadataBlockLine = i; } } }; checkPhase('metadata', PatternPhase.Metadata); checkPhase('overview', PatternPhase.Overview); checkPhase('constants', PatternPhase.Constants); checkPhase('triggers', PatternPhase.Triggers); checkPhase('body', PatternPhase.Body); if ((currentPatternPhase as PatternPhase) === PatternPhase.Metadata) { if (line.startsWith('author ')) { foundMetadata.author = true; } if (line.startsWith('publisher ')) { foundMetadata.publisher = true; } if (line.startsWith('origin ')) { foundMetadata.origin = true; } if (line.startsWith('tree_path ')) { foundMetadata.tree_path = true; } } if ((currentPatternPhase as PatternPhase) === PatternPhase.Body && !line.startsWith('body')) { bodyLineCount++; if (bodyLineCount === 1) { const expectedLog = `log.debug("Pattern start ${currentPatternName}");`; if (!line.includes(`log.debug("Pattern start ${currentPatternName}`)) { const diag = addDiag(i, 0, rawLine.length, `Body muss mit dem Log-Statement beginnen: ${expectedLog}`, vscode.DiagnosticSeverity.Error); diag.code = 'missing_log_start'; diag.source = expectedLog; } } } } // --- Innerhalb einer Tabelle --- if ((inBlock as string) === 'table') { if (!line.startsWith('end ') && !line.includes('table ')) { if (!line.includes('->') && !line.includes(':=')) { addDiag(i, 0, rawLine.length, "Ungültiger Tabelleneintrag. Nutze '[\"Key\"] -> \"Value\";' oder 'default := \"Value\";'", vscode.DiagnosticSeverity.Error); } } } // --- Allgemeine Syntax-Checks --- const eqMatch = rawLine.match(/(?=!])=(?![=])/); if (eqMatch && eqMatch.index) { if (!line.includes('"') && !line.includes("'")) { addDiag(i, eqMatch.index, eqMatch.index + 1, "Zuweisung in TPL erfolgt mit ':=', nicht mit '='. '=' ist für Vergleiche.", vscode.DiagnosticSeverity.Error); } } if (line.includes('?') && line.includes(':')) { if (tplVersion < 15) { addDiag(i, 0, rawLine.length, `Ternärer Operator (? :) erfordert mindestens TPL 1.15 (Aktuell: 1.${tplVersion}).`, vscode.DiagnosticSeverity.Error); } } const streamMatch = rawLine.match(/\bstream\b/); if (streamMatch && streamMatch.index !== undefined) { if (tplVersion < 20) { addDiag(i, streamMatch.index, streamMatch.index + 6, `Das 'stream'-Keyword erfordert mindestens TPL 1.20.`, vscode.DiagnosticSeverity.Error); } } const notInMatch = rawLine.match(/\bnot\s+in\b/); if (notInMatch && notInMatch.index !== undefined) { if (tplVersion < 18) { addDiag(i, notInMatch.index, notInMatch.index + 6, `'not in' erfordert mindestens TPL 1.18.`, vscode.DiagnosticSeverity.Error); } } // --- Block-Ausstiege --- if (line.startsWith('end pattern')) { if ((inBlock as string) !== 'pattern') { addDiag(i, 0, 11, "'end pattern' gefunden, aber es wurde kein Pattern gestartet.", vscode.DiagnosticSeverity.Error); } else if ((currentPatternPhase as PatternPhase) < PatternPhase.Body) { addDiag(i, 0, 11, "Dem Pattern fehlt ein 'body'-Block.", vscode.DiagnosticSeverity.Error); } inBlock = 'none'; } else if (line.startsWith('end table')) { if ((inBlock as string) !== 'table') { addDiag(i, 0, 9, "'end table' gefunden, aber es wurde keine Tabelle gestartet.", vscode.DiagnosticSeverity.Error); } inBlock = 'none'; } } collection.set(document.uri, diagnostics); }