erkennung für fehlende end blocks, mehrzeilige if/for/while usw erkennung um keine semikolon fehler zu melden, besseres auslesen import defines, infos werden jetzt auch im Problems fenster gezeigt, bessere autovervollständigung, automatische fehlersuche über das projekt

This commit is contained in:
kuro 2026-10-12 21:43:46 +02:00
parent eba674218e
commit ba836ee30f
14 changed files with 481 additions and 194 deletions

View File

@ -62,7 +62,13 @@
"default": 23, "default": 23,
"description": "Aktuelle TPL-Nebenversion (z. B. 23 für TPL 1.23). Wird genutzt um veraltete Module zu erkennen und beim Generieren eingefügt." "description": "Aktuelle TPL-Nebenversion (z. B. 23 für TPL 1.23). Wird genutzt um veraltete Module zu erkennen und beim Generieren eingefügt."
} }
},
"commands": [
{
"command": "tpl.scanWorkspace",
"title": "TPL: Gesamten Workspace prüfen"
} }
]
}, },
"languages": [ "languages": [
{ {

View File

@ -35,6 +35,7 @@ export class LinterContext {
public parenStack: number[] = []; public parenStack: number[] = [];
public bracketStack: number[] = []; public bracketStack: number[] = [];
public openDefines: number[] = []; public openDefines: number[] = [];
public openControlBlocks: { type: string, lineIdx: number }[] = [];
public addDiag(lineIdx: number, start: number, end: number, msg: string, severity: vscode.DiagnosticSeverity): vscode.Diagnostic { 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); const diag = new vscode.Diagnostic(new vscode.Range(lineIdx, start, lineIdx, end), msg, severity);

View File

@ -4,7 +4,6 @@ import { LinterContext, PatternPhase } from '../LinterContext';
export class BlockRule implements LinterRule { export class BlockRule implements LinterRule {
// HIER GEFIXT: 'lines: string[]' wurde zur Signatur hinzugefügt!
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void { public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void {
if (!ctx.hasModuleDef) { if (!ctx.hasModuleDef) {
if (line.startsWith('tpl ')) { ctx.hasModuleDef = true; } if (line.startsWith('tpl ')) { ctx.hasModuleDef = true; }
@ -31,8 +30,10 @@ export class BlockRule implements LinterRule {
this.handleBlockOpenings(line, rawLine, lineIdx, ctx); this.handleBlockOpenings(line, rawLine, lineIdx, ctx);
// NEU: if, for und while tracken
this.handleControlBlocks(line, rawLine, lineIdx, ctx);
if (ctx.inBlock === 'pattern') { if (ctx.inBlock === 'pattern') {
// HIER GEFIXT: 'lines' wird jetzt korrekt an die Methode weitergereicht
this.checkPatternPhases(line, rawLine, lineIdx, ctx, lines); this.checkPatternPhases(line, rawLine, lineIdx, ctx, lines);
} }
@ -43,6 +44,38 @@ export class BlockRule implements LinterRule {
for (const unclosedLine of ctx.openDefines) { for (const unclosedLine of ctx.openDefines) {
ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Block 'define' wurde nicht mit 'end define;' geschlossen!", vscode.DiagnosticSeverity.Error); 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) { private trackModuleMetadata(line: string, lineIdx: number, ctx: LinterContext) {
@ -117,7 +150,6 @@ export class BlockRule implements LinterRule {
ctx.activePhaseName = "body"; ctx.activePhaseName = "body";
ctx.activePhaseLine = lineIdx; ctx.activePhaseLine = lineIdx;
// DER LOG-CHECK IST ZURÜCK!
let foundLog = false; let foundLog = false;
for (let j = lineIdx + 1; j < lines.length; j++) { for (let j = lineIdx + 1; j < lines.length; j++) {
const aheadLine = lines[j].trim(); const aheadLine = lines[j].trim();

View File

@ -4,11 +4,47 @@ import { LinterContext } from '../LinterContext';
export class SyntaxRule implements LinterRule { export class SyntaxRule implements LinterRule {
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext): void { public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void {
this.checkTabs(rawLine, lineIdx, ctx); this.checkTabs(rawLine, lineIdx, ctx);
this.checkBrackets(line, rawLine, lineIdx, ctx);
this.checkAssignments(rawLine, lineIdx, ctx); // TODOs finden (blauer Info-Hinweis)
this.checkSemicolons(line, rawLine, lineIdx, ctx); const todoMatch = rawLine.match(/\bTODO\s*:(.*)/i);
if (todoMatch) {
ctx.addDiag(lineIdx, todoMatch.index!, rawLine.length, `TODO: ${todoMatch[1].trim()}`, vscode.DiagnosticSeverity.Information);
}
let cleanLine = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, '""');
const commentIdx = cleanLine.indexOf('//');
let codePart = commentIdx !== -1 ? cleanLine.substring(0, commentIdx).trim() : cleanLine.trim();
if (codePart.length === 0) return;
let isStructuralKeyword = /^(tpl|pattern|syncmapping|table|definitions?|define|enumeration|configuration|metadata|overview|constants|triggers|mapping|removal|body|end|on|tags|origin|author|products|publishers|tree_path|import|from)\b/.test(codePart);
if (codePart.startsWith('"') || codePart.startsWith("'")) {
isStructuralKeyword = true;
}
const isValidExecutionArea =
ctx.activePhaseName === 'body' ||
ctx.activePhaseName === 'triggers' ||
ctx.activePhaseName === 'constants' ||
ctx.activePhaseName === 'overview' ||
ctx.activePhaseName === 'mapping' ||
ctx.activePhaseName === 'metadata' ||
ctx.inModuleMetadata ||
ctx.inBlock === 'table' ||
ctx.inBlock === 'definitions' ||
ctx.openDefines.length > 0;
// 'then' und 'do' auf alleinstehenden Zeilen sind legitimer Code
if (!isStructuralKeyword && !isValidExecutionArea && !/^(then|do)\b/.test(codePart)) {
ctx.addDiag(lineIdx, 0, rawLine.length, "Ausführbarer Code ist hier nicht erlaubt. (Erlaubt in: 'body', 'triggers', 'constants', 'overview', 'mapping', 'metadata', 'definitions' oder 'define').", vscode.DiagnosticSeverity.Error);
}
this.checkBrackets(codePart, rawLine, lineIdx, ctx);
this.checkAssignments(codePart, rawLine, lineIdx, ctx);
this.checkSemicolons(codePart, rawLine, lineIdx, ctx, line);
} }
public checkEnd(lines: string[], ctx: LinterContext): void { public checkEnd(lines: string[], ctx: LinterContext): void {
@ -24,13 +60,9 @@ export class SyntaxRule implements LinterRule {
} }
} }
private checkBrackets(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) { private checkBrackets(codePart: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
let cleanLine = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, ''); for (let charIdx = 0; charIdx < codePart.length; charIdx++) {
const commentIdx = cleanLine.indexOf('//'); const char = codePart[charIdx];
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); } if (char === '(') { ctx.parenStack.push(lineIdx); }
else if (char === ')') { else if (char === ')') {
if (ctx.parenStack.length > 0) { ctx.parenStack.pop(); } if (ctx.parenStack.length > 0) { ctx.parenStack.pop(); }
@ -44,13 +76,11 @@ export class SyntaxRule implements LinterRule {
} }
} }
private checkAssignments(rawLine: string, lineIdx: number, ctx: LinterContext) { private checkAssignments(codePart: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
const lineWithoutStrings = rawLine.replace(/(["'])(?:(?=(\\?))\2.)*?\1/g, '""'); if (/\b(if|elif|where)\b/.test(codePart)) { ctx.inConditionBlock = true; }
if (/\b(if|elif|where)\b/.test(lineWithoutStrings)) { ctx.inConditionBlock = true; } if (codePart.includes('=') && !codePart.includes(':=') && !codePart.includes('>=') && !codePart.includes('<=')) {
if (!ctx.inConditionBlock && ctx.parenStack.length === 0 && ctx.bracketStack.length === 0 && !/\b(matches)\b/.test(codePart)) {
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('='); const idx = rawLine.indexOf('=');
if (idx !== -1) { if (idx !== -1) {
ctx.addDiag(lineIdx, idx, idx + 1, "Falsche Zuweisung. In TPL wird ':=' für Zuweisungen genutzt.", vscode.DiagnosticSeverity.Error); ctx.addDiag(lineIdx, idx, idx + 1, "Falsche Zuweisung. In TPL wird ':=' für Zuweisungen genutzt.", vscode.DiagnosticSeverity.Error);
@ -58,21 +88,26 @@ export class SyntaxRule implements LinterRule {
} }
} }
if (/\b(then|do)\b/.test(lineWithoutStrings) || lineWithoutStrings.endsWith(';')) { ctx.inConditionBlock = false; } if (/\b(then|do)\b/.test(codePart) || codePart.endsWith(';')) { ctx.inConditionBlock = false; }
} }
private checkSemicolons(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) { private checkSemicolons(codePart: string, rawLine: string, lineIdx: number, ctx: LinterContext, originalTrimmedLine: string) {
if (ctx.isClosingString) { if (ctx.isClosingString) {
if (ctx.stringNeedsSemicolon && !line.endsWith(';')) { if (ctx.stringNeedsSemicolon && !originalTrimmedLine.endsWith(';')) {
ctx.addDiag(lineIdx, Math.max(0, rawLine.length - 1), rawLine.length, "Fehlendes Semikolon am Ende der Zuweisung.", vscode.DiagnosticSeverity.Error); ctx.addDiag(lineIdx, Math.max(0, rawLine.length - 1), rawLine.length, "Fehlendes Semikolon am Ende der Zuweisung.", vscode.DiagnosticSeverity.Error);
} }
} else { } 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+|\/\/|\/\*|\*)/; // "then" und "do" dürfen am Anfang einer Zeile stehen, ohne ein Semikolon zu erfordern
const continuesOnNextLine = /(,|and|or|not|\+|-|\*|\/|=|:=|matches)$/i.test(line); 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+|then\b|do\b|\/\/|\/\*|\*)/.test(codePart);
if (!noSemicolonNeeded.test(line) && !ctx.justOpenedString && ctx.parenStack.length === 0 && ctx.bracketStack.length === 0 && ctx.inBlock !== 'enumeration' && !ctx.inConditionBlock && !continuesOnNextLine) { // "in" (für for-Schleifen) als gültigen Zeilenumbruch hinzugefügt
const lineToCheck = line.split('//')[0].trim(); const continuesOnNextLine = /(,|and|or|not|\+|-|\*|\/|=|:=|matches|in)$/i.test(codePart);
if (!lineToCheck.endsWith(';') && !lineToCheck.endsWith('{') && !lineToCheck.endsWith(',')) {
// HIER GEFIXT: Wenn die Zeile auf 'then' oder 'do' endet, verlangt sie kein Semikolon!
const endsWithBlockOpener = /\b(then|do)$/.test(codePart);
if (!noSemicolonNeeded && !ctx.justOpenedString && ctx.parenStack.length === 0 && ctx.bracketStack.length === 0 && ctx.inBlock !== 'enumeration' && !ctx.inConditionBlock && !continuesOnNextLine && !endsWithBlockOpener) {
if (codePart.length > 0 && !codePart.endsWith(';') && !codePart.endsWith('{') && !codePart.endsWith(',')) {
ctx.addDiag(lineIdx, Math.max(0, rawLine.length - 1), rawLine.length, "Fehlendes Semikolon am Ende der Anweisung.", vscode.DiagnosticSeverity.Error); ctx.addDiag(lineIdx, Math.max(0, rawLine.length - 1), rawLine.length, "Fehlendes Semikolon am Ende der Anweisung.", vscode.DiagnosticSeverity.Error);
} }
} }

View File

@ -9,16 +9,14 @@ export function activate(context: vscode.ExtensionContext) {
const diagnosticCollection = vscode.languages.createDiagnosticCollection('tpl-scanner'); const diagnosticCollection = vscode.languages.createDiagnosticCollection('tpl-scanner');
context.subscriptions.push(diagnosticCollection); context.subscriptions.push(diagnosticCollection);
// Linter-Events binden // Linter-Events für geöffnete/geänderte Dateien binden
vscode.workspace.onDidOpenTextDocument(doc => validateTplDocument(doc, diagnosticCollection)); vscode.workspace.onDidOpenTextDocument(doc => validateTplDocument(doc, diagnosticCollection));
vscode.workspace.onDidChangeTextDocument(event => validateTplDocument(event.document, diagnosticCollection)); vscode.workspace.onDidChangeTextDocument(event => validateTplDocument(event.document, diagnosticCollection));
// Live-Reload für Einstellungen // Live-Reload für Einstellungen (Scannt das gesamte Projekt neu)
vscode.workspace.onDidChangeConfiguration(e => { vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('tpl')) { if (e.affectsConfiguration('tpl')) {
for (const doc of vscode.workspace.textDocuments) { scanWorkspace(diagnosticCollection);
validateTplDocument(doc, diagnosticCollection);
}
} }
}); });
@ -28,8 +26,25 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(vscode.languages.registerCompletionItemProvider('tpl', new TplCompletionProvider(), '.')); context.subscriptions.push(vscode.languages.registerCompletionItemProvider('tpl', new TplCompletionProvider(), '.'));
context.subscriptions.push(vscode.languages.registerHoverProvider('tpl', new TplHoverProvider())); context.subscriptions.push(vscode.languages.registerHoverProvider('tpl', new TplHoverProvider()));
// Bei Aktivierung offene Dokumente einmal scannen // NEU: Befehl registrieren, um manuell alle Dateien zu scannen
for (const doc of vscode.workspace.textDocuments) { context.subscriptions.push(vscode.commands.registerCommand('tpl.scanWorkspace', () => {
validateTplDocument(doc, diagnosticCollection); scanWorkspace(diagnosticCollection);
vscode.window.showInformationMessage("TPL: Workspace-Scan gestartet.");
}));
// NEU: Bei Aktivierung der Extension direkt einmal alles prüfen!
scanWorkspace(diagnosticCollection);
}
// Die neue Engine für das projektweite Lesen
async function scanWorkspace(collection: vscode.DiagnosticCollection) {
// Sucht alle .tpl Dateien im gesamten geöffneten Projektordner
const files = await vscode.workspace.findFiles('**/*.tpl', '**/node_modules/**');
for (const file of files) {
// Öffnet die Datei "unsichtbar" im Hintergrund
const doc = await vscode.workspace.openTextDocument(file);
// Jagt sie durch unseren Linter
validateTplDocument(doc, collection);
} }
} }

View File

@ -8,7 +8,6 @@ export class TplCompletionProvider implements vscode.CompletionItemProvider {
private strategies: CompletionStrategy[]; private strategies: CompletionStrategy[];
constructor() { constructor() {
// Hier registrieren wir alle Completion Engines
this.strategies = [ this.strategies = [
new BuiltInCompletion(), new BuiltInCompletion(),
new SnippetCompletion(), new SnippetCompletion(),
@ -16,20 +15,18 @@ export class TplCompletionProvider implements vscode.CompletionItemProvider {
]; ];
} }
public provideCompletionItems( public async provideCompletionItems(
document: vscode.TextDocument, document: vscode.TextDocument,
position: vscode.Position, position: vscode.Position,
token: vscode.CancellationToken, token: vscode.CancellationToken,
context: vscode.CompletionContext context: vscode.CompletionContext
): vscode.ProviderResult<vscode.CompletionItem[] | vscode.CompletionList> { ): Promise<vscode.CompletionItem[]> {
// Den Text in der aktuellen Zeile bis zum Cursor holen
const linePrefix = document.lineAt(position).text.substring(0, position.character); const linePrefix = document.lineAt(position).text.substring(0, position.character);
const results: vscode.CompletionItem[] = []; const results: vscode.CompletionItem[] = [];
// Jede Engine um Vorschläge bitten
for (const strategy of this.strategies) { for (const strategy of this.strategies) {
const items = strategy.provideCompletions(document, position, linePrefix); const items = await strategy.provideCompletions(document, position, linePrefix);
if (items && items.length > 0) { if (items && items.length > 0) {
results.push(...items); results.push(...items);
} }

View File

@ -1,51 +1,77 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { tplBuiltIns } from '../data/TplBuiltIns';
export class TplHoverProvider implements vscode.HoverProvider { export class TplHoverProvider implements vscode.HoverProvider {
public async provideHover( public async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Hover | null> {
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken
): Promise<vscode.Hover | null> {
// Greift das Wort unter dem Mauszeiger ab, inklusive Punkte für Namespaces
const range = document.getWordRangeAtPosition(position, /[a-zA-Z0-9_.]+/); const range = document.getWordRangeAtPosition(position, /[a-zA-Z0-9_.]+/);
if (!range) { if (!range) return null;
return null;
} const word = document.getText(range); // z.B. "modelling.testDefine"
const text = document.getText();
const lines = text.split('\n');
const word = document.getText(range);
const parts = word.split('.'); const parts = word.split('.');
const importBase = parts[0];
const funcName = parts.length > 1 ? parts[1] : word;
let namespace = ''; // 1. Lokale Defines
let methodOrPatternName = ''; if (parts.length === 1 || parts.length === 2) {
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const defMatch = line.match(new RegExp(`^\\s*define\\s+${funcName}\\b(?:\\s*\\((.*?)\\))?`));
if (defMatch) {
const params = defMatch[1] ? defMatch[1].trim() : '';
const md = new vscode.MarkdownString();
md.appendCodeblock(`define ${funcName}(${params})`, 'tpl');
if (parts.length === 2) { const textAfterDefine = text.substring(text.indexOf(line) + line.length);
namespace = parts[0]; const docMatch = textAfterDefine.match(/^\s*(?:"""|''')([\s\S]*?)(?:"""|''')/);
methodOrPatternName = parts[1]; if (docMatch) { md.appendMarkdown(`\n\n---\n\n${docMatch[1].trim()}`); }
} else if (parts.length === 1) {
methodOrPatternName = parts[0]; return new vscode.Hover(md);
} else { }
return null; }
} }
// BMC Namespace-Methoden (z.B. discovery.listDirectory) // 2. Cross-File Hover für from ... import
if (namespace && tplBuiltIns[namespace] && tplBuiltIns[namespace][methodOrPatternName]) { const fromImportRegex = new RegExp(`from\\s+([a-zA-Z0-9_.]+)\\s+import\\s+${importBase}\\b`);
const def = tplBuiltIns[namespace][methodOrPatternName]; const fromMatch = text.match(fromImportRegex);
const markdown = new vscode.MarkdownString(); if (fromMatch) {
markdown.appendCodeblock(`${namespace}.${methodOrPatternName}`, 'tpl'); const moduleName = fromMatch[1];
markdown.appendMarkdown(`---\n\n${def.description}`);
return new vscode.Hover(markdown); // Wenn der User über die Methode fährt (modelling.XYZ)
} if (funcName !== importBase) {
// BMC Globale Funktionen (z.B. size, datatype) const files = await vscode.workspace.findFiles('**/*.tpl', '**/node_modules/**');
else if (!namespace && tplBuiltIns['built_in_globals'] && tplBuiltIns['built_in_globals'][methodOrPatternName]) { for (const file of files) {
const def = tplBuiltIns['built_in_globals'][methodOrPatternName]; const doc = await vscode.workspace.openTextDocument(file);
const markdown = new vscode.MarkdownString(); const docText = doc.getText();
markdown.appendCodeblock(`${methodOrPatternName}`, 'tpl'); if (new RegExp(`tpl\\s+1\\.\\d+\\s+module\\s+${moduleName.replace(/\./g, '\\.')}\\s*;`).test(docText)) {
markdown.appendMarkdown(`---\n\n${def.description}`); const docLines = docText.split('\n');
return new vscode.Hover(markdown); for (let i = 0; i < docLines.length; i++) {
const line = docLines[i];
const defMatch = line.match(new RegExp(`^\\s*define\\s+${funcName}\\b(?:\\s*\\((.*?)\\))?`));
if (defMatch) {
const params = defMatch[1] ? defMatch[1].trim() : '';
const md = new vscode.MarkdownString();
md.appendCodeblock(`define ${funcName}(${params})`, 'tpl');
const textAfterDefine = docText.substring(docText.indexOf(line) + line.length);
const docMatch = textAfterDefine.match(/^\s*(?:"""|''')([\s\S]*?)(?:"""|''')/);
if (docMatch) { md.appendMarkdown(`\n\n---\n\n${docMatch[1].trim()}`); }
md.appendMarkdown(`\n\n*(Importiert aus \`${moduleName}\`)*`);
return new vscode.Hover(md);
}
}
}
}
}
// Wenn er nur über "modelling" fährt
return new vscode.Hover(new vscode.MarkdownString(`Importierte Definitions **${importBase}** aus Modul \`${moduleName}\`.`));
} }
// 3. Hover für Built-Ins
if (word.startsWith('log.')) { return new vscode.Hover(new vscode.MarkdownString(`TPL Logging-Modul`)); }
if (word.startsWith('discovery.')) { return new vscode.Hover(new vscode.MarkdownString(`TPL Discovery-Modul`)); }
return null; return null;
} }
} }

View File

@ -8,14 +8,12 @@ export class MetadataFix implements TplQuickFix {
const fix = new vscode.CodeAction("Fehlende Metadaten generieren", vscode.CodeActionKind.QuickFix); const fix = new vscode.CodeAction("Fehlende Metadaten generieren", vscode.CodeActionKind.QuickFix);
fix.edit = new vscode.WorkspaceEdit(); fix.edit = new vscode.WorkspaceEdit();
// Einstellungen laden
const config = vscode.workspace.getConfiguration('tpl'); const config = vscode.workspace.getConfiguration('tpl');
const author = config.get<string>('metadata.author', 'Kuro'); const author = config.get<string>('metadata.author', 'Kuro');
const origin = config.get<string>('metadata.origin', 'Custom'); const origin = config.get<string>('metadata.origin', 'Custom');
const publisher = config.get<string>('metadata.publisher', 'IT-Operations'); const publisher = config.get<string>('metadata.publisher', 'IT-Operations');
const treePathBase = config.get<string>('metadata.treePathBase', 'Grafing'); const treePathBase = config.get<string>('metadata.treePathBase', 'Grafing');
// Herausfinden, welche Felder fehlen (steht im Diagnostic-Message-String)
const missingFieldsStr = diagnostic.message.replace('Fehlende Pflicht-Metadaten (Modul-Ebene): ', ''); const missingFieldsStr = diagnostic.message.replace('Fehlende Pflicht-Metadaten (Modul-Ebene): ', '');
const missingFields = missingFieldsStr.split(', '); const missingFields = missingFieldsStr.split(', ');
@ -26,8 +24,8 @@ export class MetadataFix implements TplQuickFix {
if (missingFields.includes('publishers')) insertText += ` publishers := '${publisher}';\n`; if (missingFields.includes('publishers')) insertText += ` publishers := '${publisher}';\n`;
if (missingFields.includes('tree_path')) insertText += ` tree_path := '${treePathBase}', 'TODO_Category', 'TODO_Subcategory';\n`; if (missingFields.includes('tree_path')) insertText += ` tree_path := '${treePathBase}', 'TODO_Category', 'TODO_Subcategory';\n`;
// Direkt über der Zeile 'end metadata;' einfügen // HIER GEFIXT: Einfügen auf Zeile + 1, damit es IN den Block rutscht
fix.edit.insert(document.uri, new vscode.Position(diagnostic.range.start.line, 0), insertText); fix.edit.insert(document.uri, new vscode.Position(diagnostic.range.start.line + 1, 0), insertText);
fix.diagnostics = [diagnostic]; fix.diagnostics = [diagnostic];
return [fix]; return [fix];

View File

@ -1,14 +1,9 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
export interface CompletionStrategy { export interface CompletionStrategy {
/**
* @param document Das aktuelle Dokument
* @param position Die Cursor-Position
* @param linePrefix Der Text der aktuellen Zeile *vor* dem Cursor
*/
provideCompletions( provideCompletions(
document: vscode.TextDocument, document: vscode.TextDocument,
position: vscode.Position, position: vscode.Position,
linePrefix: string linePrefix: string
): vscode.CompletionItem[] | undefined; ): vscode.ProviderResult<vscode.CompletionItem[] | undefined>;
} }

View File

@ -2,12 +2,103 @@ import * as vscode from 'vscode';
import { CompletionStrategy } from './CompletionStrategy'; import { CompletionStrategy } from './CompletionStrategy';
export class LocalContextCompletion implements CompletionStrategy { export class LocalContextCompletion implements CompletionStrategy {
public async provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): Promise<vscode.CompletionItem[] | undefined> {
const text = document.getText();
const items: vscode.CompletionItem[] = [];
public provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): vscode.CompletionItem[] | undefined { // 1. WENN DER USER NACH EINEM PUNKT TIPPT (Cross-File Lookup)
// TODO für Phase 2: const dotMatch = linePrefix.match(/\b([a-zA-Z0-9_]+)\.$/);
// 1. Regex Suche im Dokument nach ":=" (Variablen tracken) if (dotMatch) {
// 2. Projektweite Suche nach "define" Methoden für Autocomplete const prefix = dotMatch[1]; // z.B. "modelling"
if (prefix === 'log' || prefix === 'discovery') return undefined; // Built-ins ignorieren
return undefined; // Prüfen, ob der Präfix importiert wurde
const importRegex = new RegExp(`from\\s+([a-zA-Z0-9_.]+)\\s+import\\s+${prefix}\\b`);
const match = importRegex.exec(text);
if (match) {
const moduleName = match[1]; // z.B. "Company.software_modelling"
const externalItems = await this.resolveImportedMethods(moduleName, prefix);
return externalItems; // Nur externe Methoden zurückgeben
}
return undefined; // Unbekannter Namespace
}
// 2. LOKALE DEFINITIONS & IMPORTS PARSEN
const lines = text.split('\n');
let currentDefBlock = "";
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const defBlockMatch = line.match(/^\s*definitions?\s+([a-zA-Z0-9_]+)/);
if (defBlockMatch) { currentDefBlock = defBlockMatch[1]; continue; }
if (line.match(/^\s*end\s+definitions?/)) { currentDefBlock = ""; continue; }
const defineMatch = line.match(/^\s*define\s+([a-zA-Z0-9_]+)\b/);
if (defineMatch) {
const funcName = defineMatch[1];
const fullName = currentDefBlock ? `${currentDefBlock}.${funcName}` : funcName;
const item = new vscode.CompletionItem(fullName, vscode.CompletionItemKind.Function);
item.detail = currentDefBlock ? `Local Function in '${currentDefBlock}'` : `Local Function`;
item.insertText = new vscode.SnippetString(`${fullName}(\${1:params});$0`);
items.push(item);
}
}
// Imports für die globale Worterkennung bereitstellen
const fromImportRegex = /from\s+([a-zA-Z0-9_.]+)\s+import\s+([a-zA-Z0-9_]+)/g;
let matchVar;
while ((matchVar = fromImportRegex.exec(text)) !== null) {
const item = new vscode.CompletionItem(matchVar[2], vscode.CompletionItemKind.Module);
item.detail = `Imported Definitions from ${matchVar[1]}`;
items.push(item);
}
const varRegex = /([a-zA-Z0-9_]+)\s*:=/g;
const foundVars = new Set<string>();
while ((matchVar = varRegex.exec(text)) !== null) {
if (matchVar[1] !== 'host') { foundVars.add(matchVar[1]); }
}
for (const v of foundVars) {
items.push(new vscode.CompletionItem(v, vscode.CompletionItemKind.Variable));
}
return items;
}
// Hilfsmethode: Sucht im Workspace nach der anderen Datei!
private async resolveImportedMethods(moduleName: string, defBlockName: string): Promise<vscode.CompletionItem[]> {
const items: vscode.CompletionItem[] = [];
// Sucht alle .tpl Dateien im Projekt
const files = await vscode.workspace.findFiles('**/*.tpl', '**/node_modules/**');
for (const file of files) {
const doc = await vscode.workspace.openTextDocument(file);
const text = doc.getText();
// Ist das das gesuchte Modul? (Punkte escapen)
const moduleRegex = new RegExp(`tpl\\s+1\\.\\d+\\s+module\\s+${moduleName.replace(/\./g, '\\.')}\\s*;`);
if (moduleRegex.test(text)) {
// Modul gefunden! Jetzt die Defines im entsprechenden Block auslesen
const lines = text.split('\n');
let inTargetBlock = false;
for (const line of lines) {
if (line.match(new RegExp(`^\\s*definitions?\\s+${defBlockName}\\b`))) { inTargetBlock = true; continue; }
if (inTargetBlock && line.match(/^\s*end\s+definitions?/)) { inTargetBlock = false; break; }
if (inTargetBlock) {
const defineMatch = line.match(/^\s*define\s+([a-zA-Z0-9_]+)\b/);
if (defineMatch) {
const funcName = defineMatch[1];
const item = new vscode.CompletionItem(funcName, vscode.CompletionItemKind.Function);
item.detail = `Method from ${moduleName}`;
item.insertText = new vscode.SnippetString(`${funcName}(\${1:params});$0`);
items.push(item);
}
}
}
break; // Datei gefunden und geparst, wir können abbrechen
}
}
return items;
} }
} }

View File

@ -4,96 +4,91 @@ import { CompletionStrategy } from './CompletionStrategy';
export class SnippetCompletion implements CompletionStrategy { export class SnippetCompletion implements CompletionStrategy {
public provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): vscode.CompletionItem[] | undefined { public provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): vscode.CompletionItem[] | undefined {
// Wenn wir gerade NACH einem Punkt tippen (z.B. "log."), sind Snippets/Base-Module hier falsch! if (linePrefix.match(/\.[a-zA-Z0-9_]*$/)) { return undefined; }
if (linePrefix.match(/\.[a-zA-Z0-9_]*$/)) {
return undefined;
}
const items: vscode.CompletionItem[] = []; const items: vscode.CompletionItem[] = [];
const config = vscode.workspace.getConfiguration('tpl'); const config = vscode.workspace.getConfiguration('tpl');
// ========================================== // Kontext-Scanner: Wo befindet sich der Cursor gerade?
// 1. BASIS-NAMESPACES (discovery, log, etc.) let inBlock: 'none' | 'pattern' | 'definitions' = 'none';
// ========================================== let inPhase = '';
const namespaces = ['discovery', 'log', 'model', 'search', 'text', 'regex', 'time', 'hash', 'number', 'inference'];
for (const ns of namespaces) {
const item = new vscode.CompletionItem(ns, vscode.CompletionItemKind.Module);
item.detail = `TPL Namespace: ${ns}`;
items.push(item);
}
// ========================================== for (let i = 0; i <= position.line; i++) {
// 2. DAS GOD-SNIPPET (Skeleton) const text = document.lineAt(i).text.trim();
// ========================================== if (text.match(/^(pattern|syncmapping)\b/)) { inBlock = 'pattern'; inPhase = ''; }
const skel = new vscode.CompletionItem('tpl_skeleton', vscode.CompletionItemKind.Snippet); else if (text.match(/^definitions?\b/)) { inBlock = 'definitions'; inPhase = ''; }
const version = config.get<number>('version', 23); else if (text.match(/^end (pattern|syncmapping|definitions?)\b/)) { inBlock = 'none'; inPhase = ''; }
const author = config.get<string>('metadata.author', 'Kuro');
skel.insertText = new vscode.SnippetString( if (inBlock === 'pattern') {
`tpl 1.${version} module \${1:Name};\n\n` + ['metadata', 'overview', 'constants', 'triggers', 'mapping', 'removal', 'body'].forEach(p => {
`metadata\n` + if (text.startsWith(p)) { inPhase = p; }
` origin := '\${2:Custom}';\n` + else if (text.startsWith(`end ${p}`)) { inPhase = ''; }
` author := '${author}';\n` + });
` tree_path := 'Grafing', '\${3:Category}', '\${4:SubCategory}';\n` +
`end metadata;\n\n` +
`pattern \${5:PatternName} 1.0\n` +
` """\n \${6:Beschreibung}\n """\n` +
` overview\n tags \${7:tag1};\n end overview;\n\n` +
` triggers\n on host := Host created, confirmed;\n end triggers;\n\n` +
` body\n log.debug("Start pattern \${5:PatternName}");\n $0\n end body;\n` +
`end pattern;`
);
skel.detail = "Generiert ein komplettes TPL Grundgerüst";
items.push(skel);
// ==========================================
// 3. TPL HAUPT-BLÖCKE (pattern, define, etc.)
// ==========================================
const mainBlocks = [
{ label: 'pattern', desc: 'pattern ... end pattern;' },
{ label: 'syncmapping', desc: 'syncmapping ... end syncmapping;' },
{ label: 'define', desc: 'define ... end define;' }
];
for (const block of mainBlocks) {
const item = new vscode.CompletionItem(block.label, vscode.CompletionItemKind.Snippet);
item.insertText = new vscode.SnippetString(`${block.label} \${1:Name} \${2:1.0}\n """\n \${3:DocString}\n """\n $0\nend ${block.label};`);
if (block.label === 'define') {
item.insertText = new vscode.SnippetString(`define \${1:methodName}(\${2:params})\n """\n \${3:DocString}\n """\n $0\nend define;`);
} }
item.detail = block.desc; if (inBlock === 'definitions') {
if (text.startsWith('define ')) { inPhase = 'define'; }
else if (text.startsWith('end define')) { inPhase = ''; }
}
}
// ==========================================
// 1. GLOBAL (Top-Level: Vor dem Pattern / Import-Bereich)
// ==========================================
if (inBlock === 'none') {
const skel = new vscode.CompletionItem('tpl_skeleton', vscode.CompletionItemKind.Snippet);
skel.insertText = new vscode.SnippetString(`tpl 1.23 module \${1:Name};\n\nmetadata\n origin := '\${2:Custom}';\n tree_path := 'Grafing', '\${3:Category}', '\${4:SubCategory}';\nend metadata;\n\npattern \${5:PatternName} 1.0\n """\n \${6:Beschreibung}\n """\n overview\n tags \${7:tag1};\n end overview;\n\n triggers\n on host := Host created, confirmed;\n end triggers;\n\n body\n log.debug("Start pattern \${5:PatternName}");\n $0\n end body;\nend pattern;`);
items.push(skel);
['pattern', 'syncmapping', 'definitions'].forEach(block => {
const item = new vscode.CompletionItem(block, vscode.CompletionItemKind.Snippet);
item.insertText = new vscode.SnippetString(`${block} \${1:Name} \${2:1.0}\n """\n \${3:DocString}\n """\n $0\nend ${block};`);
items.push(item);
});
const imp = new vscode.CompletionItem('import', vscode.CompletionItemKind.Snippet);
imp.insertText = new vscode.SnippetString('import ${1:module_name};');
items.push(imp);
const fromImp = new vscode.CompletionItem('from import', vscode.CompletionItemKind.Snippet);
fromImp.insertText = new vscode.SnippetString('from ${1:module_name} import${2:function_name};');
items.push(fromImp);
}
// ==========================================
// 2. INNERHALB PATTERN (aber VOR dem Body)
// ==========================================
if (inBlock === 'pattern' && inPhase === '') {
['metadata', 'overview', 'constants', 'triggers', 'mapping', 'removal', 'body'].forEach(sec => {
const item = new vscode.CompletionItem(sec, vscode.CompletionItemKind.Snippet);
item.insertText = new vscode.SnippetString(`${sec}\n $0\nend ${sec};`);
items.push(item);
});
}
if (inBlock === 'definitions' && inPhase === '') {
const item = new vscode.CompletionItem('define', vscode.CompletionItemKind.Snippet);
item.insertText = new vscode.SnippetString(`define \${1:methodName}(\${2:params})\n """\n \${3:DocString}\n """\n $0\nend define;`);
items.push(item); items.push(item);
} }
// ========================================== // ==========================================
// 4. SECTIONS (metadata, body, triggers, etc.) // 3. INNERHALB VON BODY ODER DEFINE (Ausführbarer Code)
// ========================================== // ==========================================
const sections = ['metadata', 'overview', 'constants', 'triggers', 'mapping', 'removal', 'body']; if (inPhase === 'body' || inPhase === 'define') {
for (const sec of sections) { ['discovery', 'log', 'model', 'search', 'text', 'regex', 'time', 'hash', 'number', 'inference'].forEach(ns => {
const item = new vscode.CompletionItem(sec, vscode.CompletionItemKind.Snippet); const item = new vscode.CompletionItem(ns, vscode.CompletionItemKind.Module);
item.insertText = new vscode.SnippetString(`${sec}\n $0\nend ${sec};`); items.push(item);
item.detail = `${sec} ... end ${sec};`; });
items.push(item);
const ifSnippet = new vscode.CompletionItem('if', vscode.CompletionItemKind.Snippet);
ifSnippet.insertText = new vscode.SnippetString('if ${1:condition} then\n $0\nend if;');
items.push(ifSnippet);
const forSnippet = new vscode.CompletionItem('for', vscode.CompletionItemKind.Snippet);
forSnippet.insertText = new vscode.SnippetString('for ${1:item} in${2:list} do\n $0\nend for;');
items.push(forSnippet);
} }
// ==========================================
// 5. KONTROLLSTRUKTUREN & TABELLEN
// ==========================================
const ifSnippet = new vscode.CompletionItem('if', vscode.CompletionItemKind.Snippet);
ifSnippet.insertText = new vscode.SnippetString('if ${1:condition} then\n $0\nend if;');
ifSnippet.detail = "if ... then ... end if;";
items.push(ifSnippet);
const forSnippet = new vscode.CompletionItem('for', vscode.CompletionItemKind.Snippet);
forSnippet.insertText = new vscode.SnippetString('for ${1:item} in${2:list} do\n $0\nend for;');
forSnippet.detail = "for ... in ... do ... end for;";
items.push(forSnippet);
const tableSnippet = new vscode.CompletionItem('table', vscode.CompletionItemKind.Snippet);
tableSnippet.insertText = new vscode.SnippetString('table ${1:TableName} 1.0\n ${2:"key"} -> "${3:value}";\n default -> "unknown";\nend table;');
tableSnippet.detail = "table ... end table;";
items.push(tableSnippet);
return items; return items;
} }
} }

View File

@ -0,0 +1,34 @@
import * as assert from 'assert';
import * as vscode from 'vscode';
import { TabFix } from '../../providers/actions/TabFix';
import { LogFix } from '../../providers/actions/LogFix';
suite('Code Actions Test Suite', () => {
test('TabFix: Erzeugt Replace-Edit für Tabulatoren', () => {
const fix = new TabFix();
const diag = new vscode.Diagnostic(new vscode.Range(0, 0, 0, 1), "Tab found", vscode.DiagnosticSeverity.Warning);
// Mock für TextDocument
const mockDoc = {
uri: vscode.Uri.file('test.tpl'),
lineAt: () => ({ text: '\t\tcode', range: new vscode.Range(0, 0, 0, 6) })
} as any;
const actions = fix.provideFix(mockDoc, diag);
assert.strictEqual(actions.length, 1);
assert.strictEqual(actions[0].title, "Tabs in 4 Spaces umwandeln");
assert.ok(actions[0].edit !== undefined);
});
test('LogFix: Erzeugt Insert-Edit mit Log-Statement', () => {
const fix = new LogFix();
const diag = new vscode.Diagnostic(new vscode.Range(10, 0, 10, 4), "Missing log");
diag.source = 'log.debug("Start pattern Test");'; // Linter-Source Simulation
const mockDoc = { uri: vscode.Uri.file('test.tpl') } as any;
const actions = fix.provideFix(mockDoc, diag);
assert.strictEqual(actions.length, 1);
assert.strictEqual(actions[0].title, "Fehlendes Log-Statement einfügen");
});
});

View File

@ -0,0 +1,38 @@
import * as assert from 'assert';
import { BuiltInCompletion } from '../../providers/completions/BuiltInCompletion';
import { SnippetCompletion } from '../../providers/completions/SnippetCompletion';
suite('Completion Provider Test Suite', () => {
test('BuiltInCompletion: Reagiert auf "log."', () => {
const strategy = new BuiltInCompletion();
// Wir faken Document und Position, da wir nur linePrefix brauchen
const result = strategy.provideCompletions(null as any, null as any, ' log.');
assert.ok(result !== undefined);
assert.strictEqual(result.length, 4); // debug, info, warn, error
assert.strictEqual(result[0].label, 'debug');
});
test('BuiltInCompletion: Ignoriert normalen Text', () => {
const strategy = new BuiltInCompletion();
const result = strategy.provideCompletions(null as any, null as any, ' my_var');
assert.strictEqual(result, undefined);
});
test('SnippetCompletion: Liefert Snippets für normalen Text', () => {
const strategy = new SnippetCompletion();
const result = strategy.provideCompletions(null as any, null as any, ' i');
assert.ok(result !== undefined);
const hasIf = result.some(item => item.label === 'if');
assert.ok(hasIf, "Muss 'if' Snippet vorschlagen");
});
test('SnippetCompletion: Blockiert Snippets nach Punkt', () => {
const strategy = new SnippetCompletion();
const result = strategy.provideCompletions(null as any, null as any, ' discovery.r');
assert.strictEqual(result, undefined, "Darf keine Block-Snippets in Namespaces anzeigen");
});
});

View File

@ -1,45 +1,69 @@
import * as assert from 'assert'; import * as assert from 'assert';
import * as vscode from 'vscode';
import { LinterContext } from '../../core/LinterContext'; import { LinterContext } from '../../core/LinterContext';
import { SyntaxRule } from '../../core/rules/SyntaxRule'; import { SyntaxRule } from '../../core/rules/SyntaxRule';
import { BlockRule } from '../../core/rules/BlockRule';
suite('Linter SyntaxRule Test Suite', () => { suite('Linter Test Suite', () => {
test('Zuweisung: Warnt bei "=" statt ":="', () => { test('SyntaxRule: Warnt bei "=" Zuweisung außerhalb von Bedingungen', () => {
const rule = new SyntaxRule(); const rule = new SyntaxRule();
const ctx = new LinterContext(); const ctx = new LinterContext();
ctx.activePhaseName = 'body'; // Kontext setzen, damit der strict-mode nicht meckert
// Wir simulieren eine Code-Zeile
const rawLine = " my_var = 123;"; const rawLine = " my_var = 123;";
rule.checkLine(rawLine.trim(), rawLine, 0, ctx); // HIER GEFIXT: Wir übergeben ein Array als 5. Argument
rule.checkLine(rawLine.trim(), rawLine, 0, ctx, [rawLine]);
// Erwartung: Ein Fehler wegen falscher Zuweisung const diag = ctx.diagnostics.find(d => d.message.includes("':=' für Zuweisungen"));
assert.strictEqual(ctx.diagnostics.length, 1); assert.ok(diag !== undefined, "Fehler für falsche Zuweisung erwartet");
assert.ok(ctx.diagnostics[0].message.includes("In TPL wird ':=' für Zuweisungen genutzt"));
}); });
test('Zuweisung: Akzeptiert "=" als Vergleich in if-Bedingung', () => { test('SyntaxRule: Akzeptiert "=" als Vergleich in Bedingungen', () => {
const rule = new SyntaxRule(); const rule = new SyntaxRule();
const ctx = new LinterContext(); const ctx = new LinterContext();
ctx.activePhaseName = 'body';
// Wir simulieren eine if-Zeile (setzt ctx.inConditionBlock intern auf true)
const rawLine = " if my_var = 123 then"; const rawLine = " if my_var = 123 then";
rule.checkLine(rawLine.trim(), rawLine, 0, ctx); rule.checkLine(rawLine.trim(), rawLine, 0, ctx, [rawLine]);
// Erwartung: Kein Fehler, da das = als Vergleich legitim ist // Darf keinen Fehler werfen, da inConditionBlock automatisch auf true geht
assert.strictEqual(ctx.diagnostics.length, 0); assert.strictEqual(ctx.diagnostics.length, 0);
}); });
test('Klammern: Findet ungeschlossene runde Klammern', () => { test('SyntaxRule: Findet fehlendes Semikolon', () => {
const rule = new SyntaxRule(); const rule = new SyntaxRule();
const ctx = new LinterContext(); const ctx = new LinterContext();
ctx.activePhaseName = 'body';
// Wir öffnen eine Klammer, schließen sie aber nicht const rawLine = " my_var := 123";
const rawLine = " my_func(parameter;"; rule.checkLine(rawLine.trim(), rawLine, 0, ctx, [rawLine]);
rule.checkLine(rawLine.trim(), rawLine, 0, ctx);
// checkEnd wird am Ende der Datei aufgerufen const diag = ctx.diagnostics.find(d => d.message.includes("Fehlendes Semikolon"));
rule.checkEnd(['', rawLine], ctx); assert.ok(diag !== undefined, "Fehler für fehlendes Semikolon erwartet");
});
test('BlockRule: Erzwingt Module-Header', () => {
const rule = new BlockRule();
const ctx = new LinterContext();
const rawLine = "pattern Test 1.0";
rule.checkLine(rawLine.trim(), rawLine, 0, ctx, [rawLine]);
assert.strictEqual(ctx.diagnostics.length, 1); assert.strictEqual(ctx.diagnostics.length, 1);
assert.ok(ctx.diagnostics[0].message.includes("Öffnende Klammer '(' wird nicht geschlossen!")); assert.ok(ctx.diagnostics[0].message.includes("tpl 1.x module"));
});
test('BlockRule: Findet fehlendes Pattern-Log im Body', () => {
const rule = new BlockRule();
const ctx = new LinterContext();
ctx.hasModuleDef = true;
ctx.inBlock = 'pattern';
ctx.currentPatternName = 'TestPattern';
const lines = ['body', ' x := 1;', 'end body;'];
rule.checkLine('body', 'body', 0, ctx, lines);
const logDiag = ctx.diagnostics.find(d => d.code === 'missing_log_start');
assert.ok(logDiag !== undefined, "Erwartet Fehler für fehlendes Log-Statement");
}); });
}); });