added test cases, translated some stuff, fixed intendention rule and parameter adaptation from settings

This commit is contained in:
kuro 2026-10-27 23:38:30 +01:00
parent 43bb2fa250
commit f3506cc10b
10 changed files with 256 additions and 191 deletions

View File

@ -5,28 +5,30 @@ import { LinterContext } from '../LinterContext';
export class IndentationRule implements LinterRule {
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void {
// Leere Zeilen oder Zeilen, die nur aus Leerzeichen/Tabs bestehen, ignorieren
if (rawLine.trim().length === 0) {return;}
// Wenn Tabs drin sind, meckert die SyntaxRule bereits. Das ignorieren wir hier.
if (rawLine.includes('\t')) {return;}
// HIER IST DER FIX: Wenn wir innerhalb von Klammern sind (Visual Alignment),
// greift die 4-Leerzeichen-Regel nicht!
if (ctx.parenStack.length > 0 || ctx.bracketStack.length > 0) {
return;
}
const leadingSpacesMatch = rawLine.match(/^ +/);
const spaceCount = leadingSpacesMatch ? leadingSpacesMatch[0].length : 0;
// Ist die Anzahl der Leerzeichen kein Vielfaches von 4?
if (spaceCount > 0 && spaceCount % 4 !== 0) {
ctx.addDiag(
lineIdx,
0,
spaceCount, // Markiert genau den Bereich der Leerzeichen
`Unsaubere Einrückung (${spaceCount} Leerzeichen). Nutze ein Vielfaches von 4. (Tipp: Shift+Alt+F zum Auto-Formatieren)`,
vscode.DiagnosticSeverity.Warning // Warning reicht hier völlig, Error wäre beim Tippen zu aggressiv
spaceCount,
`Invalid indentation (${spaceCount} spaces). Please use a multiple of 4. (Tip: Shift+Alt+F to auto-format)`,
vscode.DiagnosticSeverity.Warning
);
}
}
public checkEnd(lines: string[], ctx: LinterContext): void {
// Hier muss am Ende der Datei nichts geprüft werden
// empty
}
}

View File

@ -15,13 +15,11 @@ export class MetadataRule implements LinterRule {
if (ctx.inModuleMetadata) {
const config = vscode.workspace.getConfiguration('tpl');
// Felder erfassen
if (line.match(/^origin\b/)) {ctx.foundModuleMetadata.add('origin');}
if (line.match(/^author\b/)) {ctx.foundModuleMetadata.add('author');}
if (line.match(/^products\b/)) {ctx.foundModuleMetadata.add('products');}
if (line.match(/^publishers\b/)) {ctx.foundModuleMetadata.add('publishers');}
// Tree-Path Sonderprüfung
if (line.match(/^tree_path\b/)) {
ctx.foundModuleMetadata.add('tree_path');
if (config.get<boolean>('metadata.require.treePath', true)) {
@ -29,7 +27,6 @@ export class MetadataRule implements LinterRule {
}
}
// Block-Ende: Pflichtfelder abgleichen
if (line.startsWith('end metadata')) {
ctx.inModuleMetadata = false;
this.checkRequiredFields(ctx, config);
@ -39,20 +36,19 @@ export class MetadataRule implements LinterRule {
private checkTreePath(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, config: vscode.WorkspaceConfiguration) {
const minLevels = config.get<number>('metadata.treePath.minLevels', 3);
const staticNodes = config.get<string[]>('metadata.treePath.staticNodes', ['Grafing']);
const staticNodes = config.get<string[]>('metadata.treePath.staticNodes', ['Grafing']); // Fallback
// Extrahiert die Strings aus tree_path := 'A', 'B';
const match = line.match(/tree_path\s*:=\s*(.*);/);
if (match) {
const paths = match[1].split(',').map(s => s.trim().replace(/^['"]|['"]$/g, ''));
if (paths.length < minLevels) {
ctx.addDiag(lineIdx, 0, rawLine.length, `Der tree_path muss mindestens ${minLevels} Ebenen haben!`, vscode.DiagnosticSeverity.Error);
ctx.addDiag(lineIdx, 0, rawLine.length, `The tree_path must contain at least ${minLevels} levels.`, vscode.DiagnosticSeverity.Error);
}
for (let i = 0; i < staticNodes.length; i++) {
if (paths[i] !== staticNodes[i]) {
ctx.addDiag(lineIdx, 0, rawLine.length, `Die tree_path Ebene ${i + 1} muss zwingend '${staticNodes[i]}' lauten!`, vscode.DiagnosticSeverity.Error);
ctx.addDiag(lineIdx, 0, rawLine.length, `Tree path level ${i + 1} must strictly be '${staticNodes[i]}'.`, vscode.DiagnosticSeverity.Error);
}
}
}
@ -69,7 +65,7 @@ export class MetadataRule implements LinterRule {
const missing = requiredFields.filter(f => !ctx.foundModuleMetadata.has(f));
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);
const diag = ctx.addDiag(ctx.moduleMetadataBlockLine, 0, 8, `Missing mandatory module metadata fields: ${missing.join(', ')}`, vscode.DiagnosticSeverity.Error);
diag.code = 'missing_module_metadata';
}
}

View File

@ -1,23 +1,22 @@
import * as vscode from 'vscode';
import { TplQuickFix } from './TplQuickFix';
export class LogFix implements TplQuickFix {
public readonly diagnosticCode = 'missing_log_start';
export class LogFix {
public diagnosticCode = 'missing_log_start';
public provideFix(document: vscode.TextDocument, diagnostic: vscode.Diagnostic): vscode.CodeAction[] {
const fix = new vscode.CodeAction("Fehlendes Log-Statement einfügen", vscode.CodeActionKind.QuickFix);
fix.edit = new vscode.WorkspaceEdit();
const action = new vscode.CodeAction("Insert missing log statement", vscode.CodeActionKind.QuickFix);
action.edit = new vscode.WorkspaceEdit();
// Der Linter hat uns den exakten String in die 'source' gepackt
const expectedLog = diagnostic.source || 'log.debug("Start pattern");';
const expectedLog = diagnostic.source;
// Füge es eine Zeile unter dem 'body' Keyword ein, sauber eingerückt
const insertText = ` ${expectedLog}\n`;
fix.edit.insert(document.uri, new vscode.Position(diagnostic.range.start.line + 1, 0), insertText);
fix.diagnostics = [diagnostic];
fix.isPreferred = true;
return [fix];
if (expectedLog) {
const insertPos = new vscode.Position(diagnostic.range.start.line + 1, 0);
// 8 Leerzeichen Einrückung für den Body-Inhalt
action.edit.insert(document.uri, insertPos, ` ${expectedLog}\n`);
action.diagnostics = [diagnostic];
action.isPreferred = true;
return [action];
}
return [];
}
}

View File

@ -1,20 +1,19 @@
import * as vscode from 'vscode';
import { TplQuickFix } from './TplQuickFix';
export class TabFix implements TplQuickFix {
public readonly diagnosticCode = 'invalid_tab';
export class TabFix {
public diagnosticCode = 'invalid_tab';
public provideFix(document: vscode.TextDocument, diagnostic: vscode.Diagnostic): vscode.CodeAction[] {
const fix = new vscode.CodeAction("Tabs in 4 Spaces umwandeln", vscode.CodeActionKind.QuickFix);
fix.edit = new vscode.WorkspaceEdit();
const action = new vscode.CodeAction("Convert tabs to 4 spaces", vscode.CodeActionKind.QuickFix);
action.edit = new vscode.WorkspaceEdit();
const line = document.lineAt(diagnostic.range.start.line);
const newText = line.text.replace(/\t/g, ' ');
fix.edit.replace(document.uri, line.range, newText);
fix.diagnostics = [diagnostic];
fix.isPreferred = true; // Setzt dies als Standard-Vorschlag
action.edit.replace(document.uri, line.range, newText);
action.diagnostics = [diagnostic];
action.isPreferred = true;
return [fix];
return [action];
}
}

View File

@ -1,36 +1,99 @@
import * as vscode from 'vscode';
import { CompletionStrategy } from './CompletionStrategy';
import { tplBuiltIns } from '../../data/TplBuiltIns';
import { generatedTplBuiltIns } from '../../data/GeneratedTplBuiltIns'; // Pfad ggf. anpassen
const allTplFunctions: Record<string, any> = { ...tplBuiltIns, ...generatedTplBuiltIns };
import { workspaceIndex } from '../../core/WorkspaceIndex';
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[] = [];
const dotMatch = linePrefix.match(/\b([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]*)$/);
// 1. SMART IMPORT: "from "
if (linePrefix.match(/\bfrom\s+$/)) {
for (const mod of workspaceIndex.getAllModules()) {
const item = new vscode.CompletionItem(mod, vscode.CompletionItemKind.Module);
item.detail = "Workspace Module";
items.push(item);
}
return items; // Nichts anderes vorschlagen!
}
// 2. SMART IMPORT: "from X import "
const importContextMatch = linePrefix.match(/\bfrom\s+([a-zA-Z0-9_.]+)\s+import\s+$/);
if (importContextMatch) {
const modName = importContextMatch[1];
for (const defBlock of workspaceIndex.getDefinitionBlocksForModule(modName)) {
const item = new vscode.CompletionItem(defBlock, vscode.CompletionItemKind.Class);
item.detail = `Definitions from ${modName}`;
items.push(item);
}
return items; // Nichts anderes vorschlagen!
}
// 3. NAMESPACE CALLS: "prefix."
const dotMatch = linePrefix.match(/\b([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]*)$/);
if (dotMatch) {
const prefix = dotMatch[1];
// Auf erweitertes Dictionary prüfen
const bmcNamespaces = Object.keys(allTplFunctions);
// BMC Built-ins ignorieren (übernimmt BuiltInCompletion)
const bmcNamespaces = Object.keys(tplBuiltIns);
if (bmcNamespaces.includes(prefix)) {
return undefined; // Überlässt das Feld komplett der BuiltInCompletion!
return undefined; // Hier darf undefined sein, weil BuiltInCompletion das fängt
}
let funcs = [];
// A) RAM-Abfrage für importierte Module
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];
const externalItems = await this.resolveImportedMethods(moduleName, prefix);
return externalItems;
// Filtere Funktionen aus dem importierten Modul, die zu diesem Prefix gehören
funcs = workspaceIndex.getFunctionsForModule(moduleName).filter(f => f.name.startsWith(`${prefix}.`) || !f.name.includes('.'));
} else {
// B) Lokale Definitions in der aktuellen Datei prüfen!
const localDefRegex = new RegExp(`^\\s*definitions?\\s+${prefix}\\b`, 'm');
if (localDefRegex.test(text)) {
const lines = text.split('\n');
let inTargetBlock = false;
for (const line of lines) {
if (line.match(new RegExp(`^\\s*definitions?\\s+${prefix}\\b`))) { inTargetBlock = true; continue; }
if (line.match(/^\s*end\s+definitions?/)) { inTargetBlock = false; continue; }
if (inTargetBlock) {
const defineMatch = line.match(/^\s*define\s+([a-zA-Z0-9_]+)\b(?:\s*\((.*?)\))?/);
if (defineMatch) {
const params = (defineMatch[2] || "").split(',').map(p => p.trim()).filter(p => p.length > 0);
funcs.push({ name: defineMatch[1], moduleName: 'local', params: params });
}
}
}
}
}
return undefined;
if (funcs.length > 0) {
for (const f of funcs) {
const shortName = f.name.split('.').pop() || f.name;
const item = new vscode.CompletionItem(shortName, vscode.CompletionItemKind.Function);
item.detail = f.moduleName === 'local' ? `Local Method` : `Method from ${f.moduleName}`;
// FIX: Dynamische Parameter-Generierung! Baut z.B. ${1:host}, ${2:ip}
let paramSnippet = "";
if (f.params && f.params.length > 0) {
paramSnippet = f.params.map((p, idx) => `\${${idx + 1}:${p}}`).join(', ');
}
item.insertText = new vscode.SnippetString(`${shortName}(${paramSnippet});$0`);
items.push(item);
}
return items; // Gibt exakt die Funktionen zurück
}
// FIX: Wenn es ein unbekannter Namespace mit Punkt ist, geben wir ein leeres Array [] zurück!
// Das zwingt VS Code dazu, KEINE generischen Textwörter vorzuschlagen.
return [];
}
// 4. LOKALE DEFINITIONS PARSEN (Für den globalen Scope, noch ohne Punkt aufgerufen)
const lines = text.split('\n');
let currentDefBlock = "";
@ -40,17 +103,26 @@ export class LocalContextCompletion implements CompletionStrategy {
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/);
const defineMatch = line.match(/^\s*define\s+([a-zA-Z0-9_]+)\b(?:\s*\((.*?)\))?/);
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`);
// Dynamische Parameter-Generierung auch hier!
const params = (defineMatch[2] || "").split(',').map(p => p.trim()).filter(p => p.length > 0);
let paramSnippet = "";
if (params.length > 0) {
paramSnippet = params.map((p, idx) => `\${${idx + 1}:${p}}`).join(', ');
}
item.insertText = new vscode.SnippetString(`${fullName}(${paramSnippet});$0`);
items.push(item);
}
}
// Import-Referenzen für den globalen Scope 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) {
@ -59,6 +131,7 @@ export class LocalContextCompletion implements CompletionStrategy {
items.push(item);
}
// Lokale Variablen (:=) vorschlagen
const varRegex = /([a-zA-Z0-9_]+)\s*:=/g;
const foundVars = new Set<string>();
while ((matchVar = varRegex.exec(text)) !== null) {
@ -70,37 +143,4 @@ export class LocalContextCompletion implements CompletionStrategy {
return items;
}
private async resolveImportedMethods(moduleName: string, defBlockName: string): Promise<vscode.CompletionItem[]> {
const items: vscode.CompletionItem[] = [];
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();
const moduleRegex = new RegExp(`tpl\\s+1\\.\\d+\\s+module\\s+${moduleName.replace(/\./g, '\\.')}\\s*;`);
if (moduleRegex.test(text)) {
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;
}
}
return items;
}
}

Binary file not shown.

View File

@ -4,11 +4,11 @@ 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', () => {
test('TabFix: Generates Replace-Edit for tabs', () => {
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
// Mock for TextDocument
const mockDoc = {
uri: vscode.Uri.file('test.tpl'),
lineAt: () => ({ text: '\t\tcode', range: new vscode.Range(0, 0, 0, 6) })
@ -16,11 +16,13 @@ suite('Code Actions Test Suite', () => {
const actions = fix.provideFix(mockDoc, diag);
assert.strictEqual(actions.length, 1);
assert.strictEqual(actions[0].title, "Tabs in 4 Spaces umwandeln");
// Update this string if your actual TabFix title differs!
assert.strictEqual(actions[0].title, "Convert tabs to 4 spaces");
assert.ok(actions[0].edit !== undefined);
});
test('LogFix: Erzeugt Insert-Edit mit Log-Statement', () => {
test('LogFix: Generates Insert-Edit with 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
@ -29,6 +31,8 @@ suite('Code Actions Test Suite', () => {
const actions = fix.provideFix(mockDoc, diag);
assert.strictEqual(actions.length, 1);
assert.strictEqual(actions[0].title, "Fehlendes Log-Statement einfügen");
// Update this string if your actual LogFix title differs!
assert.strictEqual(actions[0].title, "Insert missing log statement");
});
});

View File

@ -1,10 +1,9 @@
import * as assert from 'assert';
import * as vscode from 'vscode';
import { BuiltInCompletion } from '../../providers/completions/BuiltInCompletion';
import { SnippetCompletion } from '../../providers/completions/SnippetCompletion';
import { LocalContextCompletion } from '../../providers/completions/LocalContextCompletion';
import { workspaceIndex } from '../../core/WorkspaceIndex';
// Hilfsfunktion simuliert ein vscode.TextDocument
function createMockDoc(text: string): any {
const lines = text.split('\n');
return {
@ -14,51 +13,68 @@ function createMockDoc(text: string): any {
}
suite('Completion Provider Test Suite', () => {
test('SnippetCompletion: Kontext-Check (Kein if außerhalb des Bodys)', async () => {
const strategy = new SnippetCompletion();
// Simuliert, dass wir im "metadata" Block tippen
const docText = `pattern Test 1.0\nmetadata\n `;
const mockDoc = createMockDoc(docText);
const position = new vscode.Position(2, 4); // Zeile 2, Cursor am Ende
const result = await strategy.provideCompletions(mockDoc, position, ' ');
assert.ok(result !== undefined);
const hasIf = result!.some(item => item.label === 'if');
assert.strictEqual(hasIf, false, "Darf kein 'if' im metadata Block vorschlagen");
// Feeds the RAM index with a dummy file before tests
suiteSetup(() => {
const dummyUri = vscode.Uri.file('/fake/path/Company.tools.tpl');
const dummyText = `
tpl 1.23 module Company.tools;
definitions network_helpers
define ping(host, ip)
end define;
end definitions;
`.trim();
workspaceIndex.updateFile(dummyUri, dummyText);
});
test('SnippetCompletion: Vorschläge im Body korrekt', async () => {
const strategy = new SnippetCompletion();
// Simuliert, dass wir im "body" tippen
const docText = `pattern Test 1.0\nbody\n `;
const mockDoc = createMockDoc(docText);
const position = new vscode.Position(2, 4);
const result = await strategy.provideCompletions(mockDoc, position, ' ');
assert.ok(result !== undefined);
const hasIf = result!.some(item => item.label === 'if');
assert.strictEqual(hasIf, true, "Muss 'if' im Body vorschlagen");
});
test('LocalContextCompletion: Erkennt lokale defines in Namespaces', async () => {
test('LocalContextCompletion: "from " suggests only RAM modules', async () => {
const strategy = new LocalContextCompletion();
const mockDoc = createMockDoc(`from `);
const position = new vscode.Position(0, 5);
const docText = `definitions my_tools 1.0\n define doWork(host)\n end define;\nend definitions;`;
const mockDoc = createMockDoc(docText);
const position = new vscode.Position(4, 0);
const result = await strategy.provideCompletions(mockDoc, position, 'do');
const result = await strategy.provideCompletions(mockDoc, position, 'from ');
assert.ok(result !== undefined);
const funcItem = result!.find(item => item.label === 'my_tools.doWork');
assert.ok(funcItem !== undefined, "Muss die Methode inkl. Namespace extrahieren");
const hasModule = result!.some(item => item.label === 'Company.tools');
assert.strictEqual(hasModule, true, "Must suggest the module from RAM.");
});
test('LocalContextCompletion: "from X import " suggests definition blocks', async () => {
const strategy = new LocalContextCompletion();
const mockDoc = createMockDoc(`from Company.tools import `);
const position = new vscode.Position(0, 26);
const result = await strategy.provideCompletions(mockDoc, position, 'from Company.tools import ');
assert.ok(result !== undefined);
const hasBlock = result!.some(item => item.label === 'network_helpers');
assert.strictEqual(hasBlock, true, "Must suggest the definition block from the module.");
});
test('LocalContextCompletion: Blocks fallback for unknown namespace', async () => {
const strategy = new LocalContextCompletion();
const mockDoc = createMockDoc(`randomstuff.`);
const position = new vscode.Position(0, 12);
const result = await strategy.provideCompletions(mockDoc, position, 'randomstuff.');
assert.ok(result !== undefined);
assert.strictEqual(result!.length, 0, "Must return an empty array to block VS Code text spam.");
});
test('LocalContextCompletion: Imported namespace generates parameter snippet', async () => {
const strategy = new LocalContextCompletion();
const docText = `from Company.tools import network_helpers\nnetwork_helpers.`;
const mockDoc = createMockDoc(docText);
const position = new vscode.Position(1, 16);
const result = await strategy.provideCompletions(mockDoc, position, 'network_helpers.');
assert.ok(result !== undefined);
const funcItem = result!.find(item => item.label === 'ping');
assert.ok(funcItem !== undefined, "Must find the ping function");
// Prüft, ob das generierte Snippet das Auto-Semikolon und den $0 Cursor hat
const snippet = funcItem?.insertText as vscode.SnippetString;
assert.ok(snippet.value.includes('my_tools.doWork(${1:params});$0'));
assert.strictEqual(snippet.value, 'ping(${1:host}, ${2:ip});$0', "Must map parameters exactly");
});
});

View File

@ -4,49 +4,49 @@ import { IndentationRules } from '../../providers/formatters/IndentationRules';
import { AlignmentRules } from '../../providers/formatters/AlignmentRules';
suite('Formatter Test Suite', () => {
test('IndentationRules: if/then/end Block korrekt einrücken', () => {
test('IndentationRules: Indents if/then/end block correctly', () => {
const indentRules = new IndentationRules();
const state = new FormatState();
state.tabSize = 4;
// Zeile 1: if condition then (erwartet: 0 Leerzeichen, aber Level springt auf 1 für die nächste Zeile)
// Line 1: if condition then (expected: 0 spaces, but level jumps to 1 for the next line)
let result = indentRules.apply('if x = 1 then', state);
assert.strictEqual(result, 'if x = 1 then');
assert.strictEqual(state.indentLevel, 1);
// Zeile 2: normaler Code im Block (erwartet: 4 Leerzeichen)
// Line 2: normal code in block (expected: 4 spaces)
result = indentRules.apply('log.debug("test");', state);
assert.strictEqual(result, ' log.debug("test");');
assert.strictEqual(state.indentLevel, 1);
// Zeile 3: end if; (erwartet: 0 Leerzeichen, Level springt sofort vor der Ausgabe auf 0)
// Line 3: end if; (expected: 0 spaces, level jumps to 0 right before output)
result = indentRules.apply('end if;', state);
assert.strictEqual(result, 'end if;');
assert.strictEqual(state.indentLevel, 0);
});
test('IndentationRules: elif und else verhalten sich korrekt', () => {
test('IndentationRules: elif and else behave correctly', () => {
const indentRules = new IndentationRules();
const state = new FormatState();
state.tabSize = 4;
state.indentLevel = 1; // Wir starten quasi IN einem offenen if-Block
state.indentLevel = 1; // We start practically IN an open if-block
// elif muss zurückspringen (auf 0) und danach wieder auf 1 für den folgenden Block
// elif must jump back (to 0) and then back to 1 for the following block
let result = indentRules.apply('elif x = 2 then', state);
assert.strictEqual(result, 'elif x = 2 then');
assert.strictEqual(state.indentLevel, 1);
});
test('AlignmentRules: Visual Alignment an runden Klammern', () => {
test('AlignmentRules: Visual alignment at parentheses', () => {
const alignRules = new AlignmentRules();
const state = new FormatState();
// 1. Zeile mit öffnender Klammer
// 1. Line with opening parenthesis
alignRules.apply('discovery.runCommand(', 'discovery.runCommand(', state);
assert.strictEqual(state.parenAlignments.length, 1);
assert.strictEqual(state.parenAlignments[0], 21); // 21 Zeichen bis nach der offenen Klammer
assert.strictEqual(state.parenAlignments[0], 21); // 21 characters until after the open parenthesis
// 2. Zeile (Parameter soll exakt bis Spalte 21 eingerückt werden)
// 2. Line (parameter should be indented exactly to column 21)
const result = alignRules.apply('host,', 'host,', state);
assert.strictEqual(result, ' host,');
});

View File

@ -3,73 +3,82 @@ import * as vscode from 'vscode';
import { LinterContext } from '../../core/LinterContext';
import { SyntaxRule } from '../../core/rules/SyntaxRule';
import { BlockRule } from '../../core/rules/BlockRule';
import { validateTplDocument } from '../../core/TplLinter';
suite('Linter Test Suite', () => {
test('SyntaxRule: Ignoriert URLs und Sonderzeichen in Strings', () => {
test('SyntaxRule: Ignores URLs and special characters in strings', () => {
const rule = new SyntaxRule();
const ctx = new LinterContext();
ctx.activePhaseName = 'body'; // Erlaubte Zone
ctx.activePhaseName = 'body'; // Allowed zone
const rawLine = ' log.debug("https://example.com/api?q=1//test");';
rule.checkLine(rawLine.trim(), rawLine, 0, ctx, [rawLine]);
// Darf keinen Fehler werfen (weder fehlendes Semikolon noch kaputter Kommentar)
// Must not throw an error (neither missing semicolon nor broken comment)
assert.strictEqual(ctx.diagnostics.length, 0);
});
test('SyntaxRule: Erfasst TODOs als Information', () => {
const rule = new SyntaxRule();
const ctx = new LinterContext();
test('Linter: Captures TODOs as Information', () => {
const mockDoc = {
languageId: 'tpl',
getText: () => "tpl 1.0 module Test;\n// TODO: refactor this",
uri: vscode.Uri.file('test.tpl')
} as any;
const rawLine = ' // TODO: Diese Funktion refactoren';
rule.checkLine(rawLine.trim(), rawLine, 0, ctx, [rawLine]);
const collection = vscode.languages.createDiagnosticCollection('test-collection');
validateTplDocument(mockDoc, collection);
assert.strictEqual(ctx.diagnostics.length, 1);
assert.strictEqual(ctx.diagnostics[0].severity, vscode.DiagnosticSeverity.Information);
assert.ok(ctx.diagnostics[0].message.includes("TODO: Diese Funktion refactoren"));
const diags = collection.get(mockDoc.uri);
assert.strictEqual(diags?.length, 1, "Must find exactly one TODO");
assert.ok(diags![0].message.includes('TODO: refactor this'));
assert.strictEqual(diags![0].severity, vscode.DiagnosticSeverity.Information);
});
test('SyntaxRule: Multi-Line if-Bedingung ohne Semikolon-Fehler', () => {
test('SyntaxRule: Multi-line if-condition without semicolon error', () => {
const rule = new SyntaxRule();
const ctx = new LinterContext();
ctx.activePhaseName = 'body';
// Zeile 1: or-Umbruch
// Line 1: or-break
rule.checkLine('if x = 1 or', 'if x = 1 or', 0, ctx, []);
assert.strictEqual(ctx.diagnostics.length, 0);
assert.strictEqual(ctx.inConditionBlock, true);
// Zeile 2: then-Abschluss
// Line 2: then-closure
rule.checkLine('y = 2 then', 'y = 2 then', 1, ctx, []);
assert.strictEqual(ctx.diagnostics.length, 0, "'then' darf kein Semikolon am Ende verlangen!");
assert.strictEqual(ctx.diagnostics.length, 0, "'then' must not require a semicolon at the end!");
assert.strictEqual(ctx.inConditionBlock, false);
});
test('SyntaxRule: Akzeptiert Zuweisungen in erlaubten Zonen (z.B. metadata)', () => {
test('SyntaxRule: Allowed allocations in Zones (e.g. metadata)', () => {
const rule = new SyntaxRule();
const ctx = new LinterContext();
ctx.inModuleMetadata = true; // Wir sind im Modul-Metadata
ctx.inModuleMetadata = true; // We are in module metadata
const rawLine = " origin := 'Custom';";
rule.checkLine(rawLine.trim(), rawLine, 0, ctx, [rawLine]);
assert.strictEqual(ctx.diagnostics.length, 0, "Zuweisungen müssen in metadata erlaubt sein");
assert.strictEqual(ctx.diagnostics.length, 0, "Allocations have to be allowed in metadata");
});
test('BlockRule: if/for/while Stack-Tracking', () => {
const rule = new BlockRule();
const ctx = new LinterContext();
ctx.hasModuleDef = true;
ctx.inBlock = 'pattern';
test('BlockRule: if/for/while stack tracking', () => {
const mockDoc = {
languageId: 'tpl',
getText: () => "tpl 1.0 module Test;\nif condition then\nend for;",
uri: vscode.Uri.file('test.tpl')
} as any;
// Öffnet Block
rule.checkLine('if true then', 'if true then', 0, ctx, []);
assert.strictEqual(ctx.openControlBlocks.length, 1);
assert.strictEqual(ctx.openControlBlocks[0].type, 'if');
const collection = vscode.languages.createDiagnosticCollection('test-collection-blockrule');
validateTplDocument(mockDoc, collection);
// Schließt falschen Block (end for statt end if)
rule.checkLine('end for;', 'end for;', 1, ctx, []);
assert.strictEqual(ctx.diagnostics.length, 1);
assert.ok(ctx.diagnostics[0].message.includes("Falsches Ende"));
const diags = collection.get(mockDoc.uri);
// We check language-agnostically: Is there an Error exactly on line 2 (the 'end for;')?
const hasErrorOnEndFor = diags?.some(d =>
d.range.start.line === 2 &&
d.severity === vscode.DiagnosticSeverity.Error
);
assert.strictEqual(hasErrorOnEndFor, true, "Need to throw an error on 'end for;' line");
});
});