From 07eb9f7366051bcb920cdf1234f5ff848755ad21 Mon Sep 17 00:00:00 2001 From: kuro Date: Sun, 11 Oct 2026 04:31:12 +0200 Subject: [PATCH] =?UTF-8?q?Refactoring,=20alles=20in=20neue=20ordner=20ver?= =?UTF-8?q?schoben=20und=20begonnen=20neu=20zu=20unterteilen.=20dabei=20wi?= =?UTF-8?q?rd=20an=20der=20logik=20erstmal=20nichts=20ge=C3=A4ndert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/LinterContext.ts | 50 +++ src/core/TplLinter.ts | 94 ++++++ src/core/rules/BlockRule.ts | 147 +++++++++ src/core/rules/LinterRule.ts | 9 + src/core/rules/SyntaxRule.ts | 81 +++++ src/{ => data}/TplBuiltIns.ts | 0 src/extension.ts | 312 +------------------ src/{ => providers}/TplCodeActionProvider.ts | 0 src/{ => providers}/TplCompletionProvider.ts | 2 +- src/{ => providers}/TplDocumentFormatter.ts | 0 src/{ => providers}/TplHoverProvider.ts | 2 +- src/test/extension.test.ts | 15 - 12 files changed, 392 insertions(+), 320 deletions(-) create mode 100644 src/core/LinterContext.ts create mode 100644 src/core/TplLinter.ts create mode 100644 src/core/rules/BlockRule.ts create mode 100644 src/core/rules/LinterRule.ts create mode 100644 src/core/rules/SyntaxRule.ts rename src/{ => data}/TplBuiltIns.ts (100%) rename src/{ => providers}/TplCodeActionProvider.ts (100%) rename src/{ => providers}/TplCompletionProvider.ts (99%) rename src/{ => providers}/TplDocumentFormatter.ts (100%) rename src/{ => providers}/TplHoverProvider.ts (97%) delete mode 100644 src/test/extension.test.ts diff --git a/src/core/LinterContext.ts b/src/core/LinterContext.ts new file mode 100644 index 0000000..e7cb340 --- /dev/null +++ b/src/core/LinterContext.ts @@ -0,0 +1,50 @@ +import * as vscode from 'vscode'; + +export enum PatternPhase { None = 0, Metadata = 1, Overview = 2, Constants = 3, Triggers = 4, Mapping = 5, Removal = 6, Body = 7 } + +export class LinterContext { + public diagnostics: vscode.Diagnostic[] = []; + public configuredTplVersion: number = 23; + public tplVersion: number = 10; + + // Status-Tracking + public hasModuleDef = false; + public inBlock: 'none' | 'pattern' | 'table' | 'definitions' | 'enumeration' | 'configuration' = 'none'; + public currentPatternPhase: PatternPhase = PatternPhase.None; + public inMultiLineComment = false; + public inConditionBlock = false; + + // String-Tracking (Basis-Lexer) + public inTripleDouble = false; + public inTripleSingle = false; + public stringNeedsSemicolon = false; + public justOpenedString = false; + public isClosingString = false; + public isInternalStringLine = false; + + // Block- & Metadaten-Tracking + public activePhaseName = ""; + public activePhaseLine = -1; + public currentPatternName = ""; + public inModuleMetadata = false; + public hasModuleMetadata = false; + public moduleMetadataBlockLine = -1; + public foundModuleMetadata = { origin: false, author: false, products: false, publishers: false, tree_path: false }; + + // Klammer-Tracking + public parenStack: number[] = []; + public bracketStack: number[] = []; + public openDefines: number[] = []; + + public 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); + this.diagnostics.push(diag); + return diag; + } + + public resetLineStates() { + this.isInternalStringLine = false; + this.justOpenedString = false; + this.isClosingString = false; + } +} \ No newline at end of file diff --git a/src/core/TplLinter.ts b/src/core/TplLinter.ts new file mode 100644 index 0000000..946e3f4 --- /dev/null +++ b/src/core/TplLinter.ts @@ -0,0 +1,94 @@ +import * as vscode from 'vscode'; +import { LinterContext } from './LinterContext'; +import { LinterRule } from './rules/LinterRule'; +import { SyntaxRule } from './rules/SyntaxRule'; +import { BlockRule } from './rules/BlockRule'; + +export function validateTplDocument(document: vscode.TextDocument, collection: vscode.DiagnosticCollection) { + if (document.languageId !== 'tpl') { return; } + + const ctx = new LinterContext(); + const config = vscode.workspace.getConfiguration('tpl'); + ctx.configuredTplVersion = config.get('version', 23); + + const text = document.getText(); + const lines = text.split('\n'); + + // 1. Version prüfen + const versionMatch = text.match(/tpl\s+1\.(\d+)\s+module/); + if (versionMatch) { + ctx.tplVersion = parseInt(versionMatch[1], 10); + if (ctx.tplVersion < ctx.configuredTplVersion) { + const linesToSearch = text.substring(0, versionMatch.index! + 30).split('\n'); + ctx.addDiag(linesToSearch.length - 1, 0, versionMatch[0].length, `Das Modul nutzt eine alte TPL Version (1.${ctx.tplVersion}). Aktuell in den Settings definiert ist 1.${ctx.configuredTplVersion}.`, vscode.DiagnosticSeverity.Warning); + } + } + + // 2. Regeln laden + const rules: LinterRule[] = [ + new SyntaxRule(), + new BlockRule() + ]; + + // 3. Zeilenweise iterieren + for (let i = 0; i < lines.length; i++) { + const rawLine = lines[i]; + const line = rawLine.trim(); + + ctx.resetLineStates(); + + // Basis-Lexer: Strings & Kommentare tracken (läuft vor den Regeln!) + trackStrings(rawLine, line, ctx); + + if (ctx.isInternalStringLine && (ctx.inTripleDouble || ctx.inTripleSingle)) { continue; } + + if (line.startsWith('//') || line.startsWith('/*')) { + if (line.startsWith('/*')) { ctx.inMultiLineComment = true; } + continue; + } + if (ctx.inMultiLineComment) { + if (line.includes('*/')) { ctx.inMultiLineComment = false; } + continue; + } + if (line.length === 0) { continue; } + + // Regeln anwenden + for (const rule of rules) { + rule.checkLine(line, rawLine, i, ctx); + } + } + + // 4. Abschluss-Prüfungen durchführen + for (const rule of rules) { + if (rule.checkEnd) { + rule.checkEnd(lines, ctx); + } + } + + // Diagnostics an VS Code übergeben + collection.set(document.uri, ctx.diagnostics); +} + +// Interne Hilfsfunktion für den Parser +function trackStrings(rawLine: string, line: string, ctx: LinterContext) { + const tripleDoubleCount = (rawLine.match(/"""/g) || []).length; + const tripleSingleCount = (rawLine.match(/'''/g) || []).length; + + if (ctx.inTripleDouble) { + ctx.isInternalStringLine = true; + if (tripleDoubleCount % 2 !== 0) { ctx.inTripleDouble = false; ctx.isClosingString = true; } + } else if (tripleDoubleCount % 2 !== 0) { + ctx.inTripleDouble = true; ctx.justOpenedString = true; + } + + if (ctx.inTripleSingle) { + ctx.isInternalStringLine = true; + if (tripleSingleCount % 2 !== 0) { ctx.inTripleSingle = false; ctx.isClosingString = true; } + } else if (tripleSingleCount % 2 !== 0 && !ctx.justOpenedString) { + ctx.inTripleSingle = true; ctx.justOpenedString = true; + } + + if (ctx.justOpenedString) { + ctx.stringNeedsSemicolon = !line.startsWith('"""') && !line.startsWith("'''"); + } +} \ No newline at end of file diff --git a/src/core/rules/BlockRule.ts b/src/core/rules/BlockRule.ts new file mode 100644 index 0000000..f23047e --- /dev/null +++ b/src/core/rules/BlockRule.ts @@ -0,0 +1,147 @@ +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): 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); + + if (ctx.inBlock === 'pattern') { + this.checkPatternPhases(line, rawLine, lineIdx, ctx); + } + + 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); + } + } + + 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) { + 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; + } + + 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); } + } + } +} \ No newline at end of file diff --git a/src/core/rules/LinterRule.ts b/src/core/rules/LinterRule.ts new file mode 100644 index 0000000..f04174f --- /dev/null +++ b/src/core/rules/LinterRule.ts @@ -0,0 +1,9 @@ +import { LinterContext } from '../LinterContext'; + +export interface LinterRule { + // Wird für jede Code-Zeile aufgerufen + checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext): void; + + // Wird ganz am Ende aufgerufen (z.B. um ungeschlossene Klammern zu prüfen) + checkEnd?(lines: string[], ctx: LinterContext): void; +} \ No newline at end of file diff --git a/src/core/rules/SyntaxRule.ts b/src/core/rules/SyntaxRule.ts new file mode 100644 index 0000000..050faf0 --- /dev/null +++ b/src/core/rules/SyntaxRule.ts @@ -0,0 +1,81 @@ +import * as vscode from 'vscode'; +import { LinterRule } from './LinterRule'; +import { LinterContext } from '../LinterContext'; + +export class SyntaxRule implements LinterRule { + + public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext): void { + this.checkTabs(rawLine, lineIdx, ctx); + this.checkBrackets(line, rawLine, lineIdx, ctx); + this.checkAssignments(rawLine, lineIdx, ctx); + this.checkSemicolons(line, rawLine, lineIdx, ctx); + } + + public checkEnd(lines: string[], ctx: LinterContext): void { + for (const unclosedLine of ctx.parenStack) { ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Öffnende Klammer '(' wird nicht geschlossen!", vscode.DiagnosticSeverity.Error); } + for (const unclosedLine of ctx.bracketStack) { ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Öffnende eckige Klammer '[' wird nicht geschlossen!", vscode.DiagnosticSeverity.Error); } + } + + private checkTabs(rawLine: string, lineIdx: number, ctx: LinterContext) { + const tabIndex = rawLine.indexOf('\t'); + if (tabIndex !== -1) { + const diag = ctx.addDiag(lineIdx, tabIndex, tabIndex + 1, "Bitte Tabs durch Spaces ersetzen", vscode.DiagnosticSeverity.Warning); + diag.code = 'invalid_tab'; + } + } + + private checkBrackets(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) { + let cleanLine = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, ''); + const commentIdx = cleanLine.indexOf('//'); + if (commentIdx !== -1) { cleanLine = cleanLine.substring(0, commentIdx); } + + for (let charIdx = 0; charIdx < cleanLine.length; charIdx++) { + const char = cleanLine[charIdx]; + if (char === '(') { ctx.parenStack.push(lineIdx); } + else if (char === ')') { + if (ctx.parenStack.length > 0) { ctx.parenStack.pop(); } + else { ctx.addDiag(lineIdx, 0, rawLine.length, "Unerwartete schließende Klammer ')'. Es fehlt eine öffnende Klammer.", vscode.DiagnosticSeverity.Error); } + } + else if (char === '[') { ctx.bracketStack.push(lineIdx); } + else if (char === ']') { + if (ctx.bracketStack.length > 0) { ctx.bracketStack.pop(); } + else { ctx.addDiag(lineIdx, 0, rawLine.length, "Unerwartete schließende eckige Klammer ']'. Es fehlt eine öffnende Klammer.", vscode.DiagnosticSeverity.Error); } + } + } + } + + private checkAssignments(rawLine: string, lineIdx: number, ctx: LinterContext) { + const lineWithoutStrings = rawLine.replace(/(["'])(?:(?=(\\?))\2.)*?\1/g, '""'); + + if (/\b(if|elif|where)\b/.test(lineWithoutStrings)) { ctx.inConditionBlock = true; } + + if (lineWithoutStrings.includes('=') && !lineWithoutStrings.includes(':=') && !lineWithoutStrings.includes('>=') && !lineWithoutStrings.includes('<=')) { + if (!ctx.inConditionBlock && ctx.parenStack.length === 0 && ctx.bracketStack.length === 0 && !/\b(matches)\b/.test(lineWithoutStrings)) { + const idx = rawLine.indexOf('='); + if (idx !== -1) { + ctx.addDiag(lineIdx, idx, idx + 1, "Falsche Zuweisung. In TPL wird ':=' für Zuweisungen genutzt.", vscode.DiagnosticSeverity.Error); + } + } + } + + if (/\b(then|do)\b/.test(lineWithoutStrings) || lineWithoutStrings.endsWith(';')) { ctx.inConditionBlock = false; } + } + + private checkSemicolons(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) { + if (ctx.isClosingString) { + if (ctx.stringNeedsSemicolon && !line.endsWith(';')) { + ctx.addDiag(lineIdx, Math.max(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) && !ctx.justOpenedString && ctx.parenStack.length === 0 && ctx.bracketStack.length === 0 && ctx.inBlock !== 'enumeration' && !ctx.inConditionBlock && !continuesOnNextLine) { + const lineToCheck = line.split('//')[0].trim(); + if (!lineToCheck.endsWith(';') && !lineToCheck.endsWith('{') && !lineToCheck.endsWith(',')) { + ctx.addDiag(lineIdx, Math.max(0, rawLine.length - 1), rawLine.length, "Fehlendes Semikolon am Ende der Anweisung.", vscode.DiagnosticSeverity.Error); + } + } + } + } +} \ No newline at end of file diff --git a/src/TplBuiltIns.ts b/src/data/TplBuiltIns.ts similarity index 100% rename from src/TplBuiltIns.ts rename to src/data/TplBuiltIns.ts diff --git a/src/extension.ts b/src/extension.ts index b4af977..7e8a3cf 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,19 +1,19 @@ import * as vscode from 'vscode'; -import { TplCodeActionProvider } from './TplCodeActionProvider'; -import { TplDocumentFormatter } from './TplDocumentFormatter'; -import { TplCompletionProvider } from './TplCompletionProvider'; -import { TplHoverProvider } from './TplHoverProvider'; - -enum PatternPhase { None = 0, Metadata = 1, Overview = 2, Constants = 3, Triggers = 4, Mapping = 5, Removal = 6, Body = 7 } +import { validateTplDocument } from './core/TplLinter'; +import { TplCodeActionProvider } from './providers/TplCodeActionProvider'; +import { TplDocumentFormatter } from './providers/TplDocumentFormatter'; +import { TplCompletionProvider } from './providers/TplCompletionProvider'; +import { TplHoverProvider } from './providers/TplHoverProvider'; export function activate(context: vscode.ExtensionContext) { const diagnosticCollection = vscode.languages.createDiagnosticCollection('tpl-scanner'); context.subscriptions.push(diagnosticCollection); + // Linter-Events binden vscode.workspace.onDidOpenTextDocument(doc => validateTplDocument(doc, diagnosticCollection)); vscode.workspace.onDidChangeTextDocument(event => validateTplDocument(event.document, diagnosticCollection)); - // Sofort reagieren, wenn in den VS Code Einstellungen etwas geändert wird + // Live-Reload für Einstellungen vscode.workspace.onDidChangeConfiguration(e => { if (e.affectsConfiguration('tpl')) { for (const doc of vscode.workspace.textDocuments) { @@ -22,308 +22,14 @@ export function activate(context: vscode.ExtensionContext) { } }); + // Provider registrieren 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())); + // Bei Aktivierung offene Dokumente einmal scannen 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; } - - // --- HIER IST DER KORRIGIERTE LINTER FÜR ZUWEISUNGEN --- - const lineWithoutStrings = rawLine.replace(/(["'])(?:(?=(\\?))\2.)*?\1/g, '""'); - - // 1. Schaltet den Condition-Block EIN - if (/\b(if|elif|where)\b/.test(lineWithoutStrings)) { - inConditionBlock = true; - } - - // 2. "=" PRÜFUNG (solange der Block noch aktiv ist) - 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, "Falsche Zuweisung. In TPL wird ':=' für Zuweisungen genutzt.", vscode.DiagnosticSeverity.Error); - } - } - } - - // 3. Schaltet den Condition-Block wieder AUS (greift für die nächste Zeile) - 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 (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); } \ No newline at end of file diff --git a/src/TplCodeActionProvider.ts b/src/providers/TplCodeActionProvider.ts similarity index 100% rename from src/TplCodeActionProvider.ts rename to src/providers/TplCodeActionProvider.ts diff --git a/src/TplCompletionProvider.ts b/src/providers/TplCompletionProvider.ts similarity index 99% rename from src/TplCompletionProvider.ts rename to src/providers/TplCompletionProvider.ts index dc5ddd9..1022938 100644 --- a/src/TplCompletionProvider.ts +++ b/src/providers/TplCompletionProvider.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import { tplBuiltIns } from './TplBuiltIns'; +import { tplBuiltIns } from '../data/TplBuiltIns'; export class TplCompletionProvider implements vscode.CompletionItemProvider { async provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { diff --git a/src/TplDocumentFormatter.ts b/src/providers/TplDocumentFormatter.ts similarity index 100% rename from src/TplDocumentFormatter.ts rename to src/providers/TplDocumentFormatter.ts diff --git a/src/TplHoverProvider.ts b/src/providers/TplHoverProvider.ts similarity index 97% rename from src/TplHoverProvider.ts rename to src/providers/TplHoverProvider.ts index 7b4cbf6..6daf930 100644 --- a/src/TplHoverProvider.ts +++ b/src/providers/TplHoverProvider.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import { tplBuiltIns } from './TplBuiltIns'; +import { tplBuiltIns } from '../data/TplBuiltIns'; export class TplHoverProvider implements vscode.HoverProvider { public async provideHover( diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts deleted file mode 100644 index 4ca0ab4..0000000 --- a/src/test/extension.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as assert from 'assert'; - -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it -import * as vscode from 'vscode'; -// import * as myExtension from '../../extension'; - -suite('Extension Test Suite', () => { - vscode.window.showInformationMessage('Start all tests.'); - - test('Sample test', () => { - assert.strictEqual(-1, [1, 2, 3].indexOf(5)); - assert.strictEqual(-1, [1, 2, 3].indexOf(0)); - }); -});