import * as vscode from 'vscode'; import { TplCodeActionProvider } from './TplCodeActionProvider'; import { TplDocumentFormatter } from './TplDocumentFormatter'; import { TplCompletionProvider } from './TplCompletionProvider'; import { TplHoverProvider } from './TplHoverProvider'; import { tplBuiltIns } from './TplBuiltIns'; enum PatternPhase { None = 0, Metadata = 1, Overview = 2, Constants = 3, Triggers = 4, Mapping = 5, Removal = 6, Body = 7 } export function activate(context: vscode.ExtensionContext) { const diagnosticCollection = vscode.languages.createDiagnosticCollection('tpl-scanner'); context.subscriptions.push(diagnosticCollection); vscode.workspace.onDidOpenTextDocument(doc => validateTplDocument(doc, diagnosticCollection)); vscode.workspace.onDidChangeTextDocument(event => validateTplDocument(event.document, diagnosticCollection)); // NEU: Sofort reagieren, wenn in den VS Code Einstellungen etwas geändert wird vscode.workspace.onDidChangeConfiguration(e => { if (e.affectsConfiguration('tpl')) { for (const doc of vscode.workspace.textDocuments) { validateTplDocument(doc, diagnosticCollection); } } }); context.subscriptions.push(vscode.languages.registerCodeActionsProvider('tpl', new TplCodeActionProvider(), { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] })); context.subscriptions.push(vscode.languages.registerDocumentFormattingEditProvider('tpl', new TplDocumentFormatter())); context.subscriptions.push(vscode.languages.registerCompletionItemProvider('tpl', new TplCompletionProvider(), '.')); context.subscriptions.push(vscode.languages.registerHoverProvider('tpl', new TplHoverProvider())); for (const doc of vscode.workspace.textDocuments) { validateTplDocument(doc, diagnosticCollection); } } function validateTplDocument(document: vscode.TextDocument, collection: vscode.DiagnosticCollection) { if (document.languageId !== 'tpl') { return; } const config = vscode.workspace.getConfiguration('tpl'); const configuredTplVersion = config.get('version', 23); const diagnostics: vscode.Diagnostic[] = []; const text = document.getText(); const lines = text.split('\n'); let tplVersion = 10; let hasModuleDef = false; let inBlock: 'none' | 'pattern' | 'table' | 'definitions' | 'enumeration' | 'configuration' = 'none'; let currentPatternPhase: PatternPhase = PatternPhase.None; let inMultiLineComment = false; let inTripleDouble = false; let inTripleSingle = false; let stringNeedsSemicolon = false; let activePhaseName = ""; let activePhaseLine = -1; let currentPatternName = ""; // Modul-Ebene Metadaten Tracking let inModuleMetadata = false; let hasModuleMetadata = false; let moduleMetadataBlockLine = -1; let foundModuleMetadata = { origin: false, author: false, products: false, publishers: false, tree_path: false }; const parenStack: number[] = []; const bracketStack: number[] = []; const openDefines: number[] = []; // State-Tracking für mehrzeilige Vergleiche / Zuweisungen let inConditionBlock = false; function addDiag(lineIdx: number, start: number, end: number, msg: string, severity: vscode.DiagnosticSeverity): vscode.Diagnostic { const diag = new vscode.Diagnostic(new vscode.Range(lineIdx, start, lineIdx, end), msg, severity); diagnostics.push(diag); return diag; } // Version prüfen const versionMatch = text.match(/tpl\s+1\.(\d+)\s+module/); if (versionMatch) { tplVersion = parseInt(versionMatch[1], 10); // Warnung nur, wenn die Datei älter ist als die in den Settings eingestellte Version! Höher ist in Ordnung. if (tplVersion < configuredTplVersion) { const linesToSearch = text.substring(0, versionMatch.index! + 30).split('\n'); const lineIdx = linesToSearch.length - 1; addDiag(lineIdx, 0, versionMatch[0].length, `Das Modul nutzt eine alte TPL Version (1.${tplVersion}). Aktuell in den Settings definiert ist 1.${configuredTplVersion}.`, vscode.DiagnosticSeverity.Warning); } } for (let i = 0; i < lines.length; i++) { const rawLine = lines[i]; const line = rawLine.trim(); const tripleDoubleCount = (rawLine.match(/"""/g) || []).length; const tripleSingleCount = (rawLine.match(/'''/g) || []).length; let isInternalStringLine = false; let justOpenedString = false; let isClosingString = false; if (inTripleDouble) { isInternalStringLine = true; if (tripleDoubleCount % 2 !== 0) { inTripleDouble = false; isClosingString = true; } } else if (tripleDoubleCount % 2 !== 0) { inTripleDouble = true; justOpenedString = true; } if (inTripleSingle) { isInternalStringLine = true; if (tripleSingleCount % 2 !== 0) { inTripleSingle = false; isClosingString = true; } } else if (tripleSingleCount % 2 !== 0 && !justOpenedString) { inTripleSingle = true; justOpenedString = true; } if (isInternalStringLine && (inTripleDouble || inTripleSingle)) { continue; } if (justOpenedString) { stringNeedsSemicolon = !line.startsWith('"""') && !line.startsWith("'''"); } const tabIndex = rawLine.indexOf('\t'); if (tabIndex !== -1) { const diag = addDiag(i, tabIndex, tabIndex + 1, "Bitte Tabs durch Spaces ersetzen", vscode.DiagnosticSeverity.Warning); diag.code = 'invalid_tab'; } let cleanLineForParens = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, ''); const commentIdx = cleanLineForParens.indexOf('//'); if (commentIdx !== -1) { cleanLineForParens = cleanLineForParens.substring(0, commentIdx); } for (let charIdx = 0; charIdx < cleanLineForParens.length; charIdx++) { const char = cleanLineForParens[charIdx]; if (char === '(') { parenStack.push(i); } else if (char === ')') { if (parenStack.length > 0) { parenStack.pop(); } else { addDiag(i, 0, rawLine.length, "Unerwartete schließende Klammer ')'. Es fehlt eine öffnende Klammer.", vscode.DiagnosticSeverity.Error); } } else if (char === '[') { bracketStack.push(i); } else if (char === ']') { if (bracketStack.length > 0) { bracketStack.pop(); } else { addDiag(i, 0, rawLine.length, "Unerwartete schließende eckige Klammer ']'. Es fehlt eine öffnende Klammer.", vscode.DiagnosticSeverity.Error); } } } if (line.startsWith('//') || line.startsWith('/*')) { if (line.startsWith('/*')) {inMultiLineComment = true;} continue; } if (inMultiLineComment) { if (line.includes('*/')) {inMultiLineComment = false;} continue; } if (line.length === 0) { continue; } const lineWithoutStrings = rawLine.replace(/(["'])(?:(?=(\\?))\2.)*?\1/g, '""'); if (/\b(if|elif|where)\b/.test(lineWithoutStrings)) { inConditionBlock = true; } if (/\b(then|do)\b/.test(lineWithoutStrings) || lineWithoutStrings.endsWith(';')) { inConditionBlock = false; } if (isClosingString) { if (stringNeedsSemicolon && !line.endsWith(';')) { addDiag(i, rawLine.length - 1 < 0 ? 0 : rawLine.length - 1, rawLine.length, "Fehlendes Semikolon am Ende der Zuweisung.", vscode.DiagnosticSeverity.Error); } } else { const noSemicolonNeeded = /^(pattern|syncmapping|traverse|table|enumeration|configuration|definitions?|define|metadata|overview|constants|triggers|mapping|removal|body|end\s+|if\s+|else|elif\s+|for\s+|while\s+|\/\/|\/\*|\*)/; const continuesOnNextLine = /(,|and|or|not|\+|-|\*|\/|=|:=|matches)$/i.test(line); if (!noSemicolonNeeded.test(line) && !justOpenedString && parenStack.length === 0 && bracketStack.length === 0 && inBlock !== 'enumeration' && !inConditionBlock && !continuesOnNextLine) { const lineToCheck = line.split('//')[0].trim(); if (!lineToCheck.endsWith(';') && !lineToCheck.endsWith('{') && !lineToCheck.endsWith(',')) { addDiag(i, rawLine.length - 1 < 0 ? 0 : rawLine.length - 1, rawLine.length, "Fehlendes Semikolon am Ende der Anweisung.", vscode.DiagnosticSeverity.Error); } } } 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; } } if (line.startsWith('metadata') && inBlock === 'none') { inModuleMetadata = true; hasModuleMetadata = true; moduleMetadataBlockLine = i; continue; } if (inModuleMetadata) { if (line.match(/^origin\b/)) { foundModuleMetadata.origin = true; } if (line.match(/^author\b/)) { foundModuleMetadata.author = true; } if (line.match(/^products\b/)) { foundModuleMetadata.products = true; } if (line.match(/^publishers\b/)) { foundModuleMetadata.publishers = true; } if (line.match(/^tree_path\b/)) { foundModuleMetadata.tree_path = true; } if (line.startsWith('end metadata')) { inModuleMetadata = false; const missing = Object.keys(foundModuleMetadata).filter(k => !(foundModuleMetadata as any)[k]); if (missing.length > 0 && moduleMetadataBlockLine !== -1) { const diag = addDiag(moduleMetadataBlockLine, 0, 8, `Fehlende Pflicht-Metadaten (Modul-Ebene): ${missing.join(', ')}`, vscode.DiagnosticSeverity.Error); diag.code = 'missing_module_metadata'; } } continue; } if (line.match(/^(pattern|syncmapping|table|definitions?|enumeration|configuration)\b/)) { if (!hasModuleMetadata && inBlock === 'none') { addDiag(i, 0, line.length, "Ein 'metadata' Block auf Modul-Ebene ist Pflicht, bevor andere Blöcke definiert werden.", vscode.DiagnosticSeverity.Error); } } if (line.startsWith('definitions ') || line.startsWith('definition ')) { if (inBlock !== 'none') { addDiag(i, 0, line.length, "Ein Definitions-Block muss auf oberster Ebene deklariert werden.", vscode.DiagnosticSeverity.Error); } inBlock = 'definitions'; continue; } if (line.startsWith('define ')) { openDefines.push(i); continue; } if (line.startsWith('enumeration ')) { if (inBlock !== 'none') { addDiag(i, 0, line.length, "Eine Enumeration muss auf oberster Ebene deklariert werden.", vscode.DiagnosticSeverity.Error); } inBlock = 'enumeration'; continue; } if (line.startsWith('configuration ')) { if (inBlock !== 'none') { addDiag(i, 0, line.length, "Eine Konfiguration muss auf oberster Ebene deklariert werden.", vscode.DiagnosticSeverity.Error); } inBlock = 'configuration'; continue; } if (line.startsWith('pattern ') || line.startsWith('syncmapping ')) { if (inBlock !== 'none') { addDiag(i, 0, 7, "Ein Pattern/Syncmapping kann nicht innerhalb eines anderen Blocks deklariert werden.", vscode.DiagnosticSeverity.Error); } inBlock = 'pattern'; currentPatternPhase = PatternPhase.None; activePhaseName = ""; activePhaseLine = -1; const nameMatch = line.match(/(pattern|syncmapping)\s+([a-zA-Z0-9_]+)\s+\d+\.\d+/); if (nameMatch) { currentPatternName = nameMatch[2]; } else { addDiag(i, 0, rawLine.length, "Ungültige Pattern-Deklaration. Erwartet: 'pattern '", vscode.DiagnosticSeverity.Error); } continue; } if (line.startsWith('table ')) { if (inBlock !== 'none') { addDiag(i, 0, 5, "Eine Tabelle kann nicht innerhalb eines anderen Blocks deklariert werden.", vscode.DiagnosticSeverity.Error); } inBlock = 'table'; continue; } if (inBlock === 'pattern') { const checkPhase = (phaseName: string, phaseEnum: PatternPhase) => { if (line.startsWith(phaseName)) { if (activePhaseName !== "") { addDiag(activePhaseLine, 0, activePhaseName.length, `Block '${activePhaseName}' wurde nicht mit 'end ${activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); } if (currentPatternPhase >= phaseEnum) { addDiag(i, 0, phaseName.length, `'${phaseName}' Block ist hier nicht erlaubt.`, vscode.DiagnosticSeverity.Error); } currentPatternPhase = phaseEnum; activePhaseName = phaseName; activePhaseLine = i; } }; 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 (activePhaseName !== "") { addDiag(activePhaseLine, 0, activePhaseName.length, `Block '${activePhaseName}' wurde nicht mit 'end ${activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); } activePhaseName = "body"; activePhaseLine = i; let foundLog = false; for (let j = i + 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(currentPatternName)) { foundLog = true; break; } } if (!foundLog) { const expectedLog = `log.debug("Start pattern ${currentPatternName}");`; const diag = addDiag(i, 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 (activePhaseName !== phase) { addDiag(i, 0, line.length, `Unerwartetes '${closer}'. Erwartet wurde 'end ${activePhaseName};'.`, vscode.DiagnosticSeverity.Error); } else { activePhaseName = ""; } } } } if (lineWithoutStrings.includes('=') && !lineWithoutStrings.includes(':=') && !lineWithoutStrings.includes('>=') && !lineWithoutStrings.includes('<=')) { if (!inConditionBlock && parenStack.length === 0 && bracketStack.length === 0 && !/\b(matches)\b/.test(lineWithoutStrings)) { const idx = rawLine.indexOf('='); if (idx !== -1) { addDiag(i, idx, idx + 1, "Mögliche falsche Zuweisung. In TPL wird ':=' für Zuweisungen genutzt. (Toleriert in Vergleichen/Bedingungen)", vscode.DiagnosticSeverity.Warning); } } } if (line.startsWith('end pattern') || line.startsWith('end syncmapping')) { if (activePhaseName !== "") { addDiag(activePhaseLine, 0, activePhaseName.length, `Block '${activePhaseName}' wurde nicht mit 'end ${activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); } if (inBlock !== 'pattern') { addDiag(i, 0, 11, "'end pattern' gefunden, ohne offenen Pattern-Block.", vscode.DiagnosticSeverity.Error); } inBlock = 'none'; } else if (line.startsWith('end table') || line.startsWith('end enumeration') || line.startsWith('end configuration')) { inBlock = 'none'; } else if (line.startsWith('end definitions') || line.startsWith('end definition')) { if (inBlock !== 'definitions') { addDiag(i, 0, line.length, "'end definitions' gefunden, ohne offenen Definitions-Block.", vscode.DiagnosticSeverity.Error); } inBlock = 'none'; } else if (line.startsWith('end define')) { if (openDefines.length > 0) { openDefines.pop(); } else { addDiag(i, 0, line.length, "'end define' gefunden, ohne offenes 'define'.", vscode.DiagnosticSeverity.Error); } } } for (const unclosedLine of parenStack) { addDiag(unclosedLine, 0, lines[unclosedLine].length, "Öffnende Klammer '(' wird nicht geschlossen!", vscode.DiagnosticSeverity.Error); } for (const unclosedLine of bracketStack) { addDiag(unclosedLine, 0, lines[unclosedLine].length, "Öffnende eckige Klammer '[' wird nicht geschlossen!", vscode.DiagnosticSeverity.Error); } for (const unclosedLine of openDefines) { addDiag(unclosedLine, 0, lines[unclosedLine].length, "Block 'define' wurde nicht mit 'end define;' geschlossen!", vscode.DiagnosticSeverity.Error); } collection.set(document.uri, diagnostics); }