added test cases, translated some stuff, fixed intendention rule and parameter adaptation from settings
This commit is contained in:
parent
43bb2fa250
commit
f3506cc10b
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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,');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue