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 {
|
export class IndentationRule implements LinterRule {
|
||||||
|
|
||||||
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 {
|
||||||
// Leere Zeilen oder Zeilen, die nur aus Leerzeichen/Tabs bestehen, ignorieren
|
|
||||||
if (rawLine.trim().length === 0) {return;}
|
if (rawLine.trim().length === 0) {return;}
|
||||||
|
|
||||||
// Wenn Tabs drin sind, meckert die SyntaxRule bereits. Das ignorieren wir hier.
|
|
||||||
if (rawLine.includes('\t')) {return;}
|
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 leadingSpacesMatch = rawLine.match(/^ +/);
|
||||||
const spaceCount = leadingSpacesMatch ? leadingSpacesMatch[0].length : 0;
|
const spaceCount = leadingSpacesMatch ? leadingSpacesMatch[0].length : 0;
|
||||||
|
|
||||||
// Ist die Anzahl der Leerzeichen kein Vielfaches von 4?
|
|
||||||
if (spaceCount > 0 && spaceCount % 4 !== 0) {
|
if (spaceCount > 0 && spaceCount % 4 !== 0) {
|
||||||
ctx.addDiag(
|
ctx.addDiag(
|
||||||
lineIdx,
|
lineIdx,
|
||||||
0,
|
0,
|
||||||
spaceCount, // Markiert genau den Bereich der Leerzeichen
|
spaceCount,
|
||||||
`Unsaubere Einrückung (${spaceCount} Leerzeichen). Nutze ein Vielfaches von 4. (Tipp: Shift+Alt+F zum Auto-Formatieren)`,
|
`Invalid indentation (${spaceCount} spaces). Please use a multiple of 4. (Tip: Shift+Alt+F to auto-format)`,
|
||||||
vscode.DiagnosticSeverity.Warning // Warning reicht hier völlig, Error wäre beim Tippen zu aggressiv
|
vscode.DiagnosticSeverity.Warning
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public checkEnd(lines: string[], ctx: LinterContext): void {
|
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) {
|
if (ctx.inModuleMetadata) {
|
||||||
const config = vscode.workspace.getConfiguration('tpl');
|
const config = vscode.workspace.getConfiguration('tpl');
|
||||||
|
|
||||||
// Felder erfassen
|
|
||||||
if (line.match(/^origin\b/)) {ctx.foundModuleMetadata.add('origin');}
|
if (line.match(/^origin\b/)) {ctx.foundModuleMetadata.add('origin');}
|
||||||
if (line.match(/^author\b/)) {ctx.foundModuleMetadata.add('author');}
|
if (line.match(/^author\b/)) {ctx.foundModuleMetadata.add('author');}
|
||||||
if (line.match(/^products\b/)) {ctx.foundModuleMetadata.add('products');}
|
if (line.match(/^products\b/)) {ctx.foundModuleMetadata.add('products');}
|
||||||
if (line.match(/^publishers\b/)) {ctx.foundModuleMetadata.add('publishers');}
|
if (line.match(/^publishers\b/)) {ctx.foundModuleMetadata.add('publishers');}
|
||||||
|
|
||||||
// Tree-Path Sonderprüfung
|
|
||||||
if (line.match(/^tree_path\b/)) {
|
if (line.match(/^tree_path\b/)) {
|
||||||
ctx.foundModuleMetadata.add('tree_path');
|
ctx.foundModuleMetadata.add('tree_path');
|
||||||
if (config.get<boolean>('metadata.require.treePath', true)) {
|
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')) {
|
if (line.startsWith('end metadata')) {
|
||||||
ctx.inModuleMetadata = false;
|
ctx.inModuleMetadata = false;
|
||||||
this.checkRequiredFields(ctx, config);
|
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) {
|
private checkTreePath(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, config: vscode.WorkspaceConfiguration) {
|
||||||
const minLevels = config.get<number>('metadata.treePath.minLevels', 3);
|
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*(.*);/);
|
const match = line.match(/tree_path\s*:=\s*(.*);/);
|
||||||
if (match) {
|
if (match) {
|
||||||
const paths = match[1].split(',').map(s => s.trim().replace(/^['"]|['"]$/g, ''));
|
const paths = match[1].split(',').map(s => s.trim().replace(/^['"]|['"]$/g, ''));
|
||||||
|
|
||||||
if (paths.length < minLevels) {
|
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++) {
|
for (let i = 0; i < staticNodes.length; i++) {
|
||||||
if (paths[i] !== staticNodes[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));
|
const missing = requiredFields.filter(f => !ctx.foundModuleMetadata.has(f));
|
||||||
|
|
||||||
if (missing.length > 0 && ctx.moduleMetadataBlockLine !== -1) {
|
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';
|
diag.code = 'missing_module_metadata';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,22 @@
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { TplQuickFix } from './TplQuickFix';
|
|
||||||
|
|
||||||
export class LogFix implements TplQuickFix {
|
export class LogFix {
|
||||||
public readonly diagnosticCode = 'missing_log_start';
|
public diagnosticCode = 'missing_log_start';
|
||||||
|
|
||||||
public provideFix(document: vscode.TextDocument, diagnostic: vscode.Diagnostic): vscode.CodeAction[] {
|
public provideFix(document: vscode.TextDocument, diagnostic: vscode.Diagnostic): vscode.CodeAction[] {
|
||||||
const fix = new vscode.CodeAction("Fehlendes Log-Statement einfügen", vscode.CodeActionKind.QuickFix);
|
const action = new vscode.CodeAction("Insert missing log statement", vscode.CodeActionKind.QuickFix);
|
||||||
fix.edit = new vscode.WorkspaceEdit();
|
action.edit = new vscode.WorkspaceEdit();
|
||||||
|
|
||||||
// Der Linter hat uns den exakten String in die 'source' gepackt
|
const expectedLog = diagnostic.source;
|
||||||
const expectedLog = diagnostic.source || 'log.debug("Start pattern");';
|
|
||||||
|
|
||||||
// Füge es eine Zeile unter dem 'body' Keyword ein, sauber eingerückt
|
if (expectedLog) {
|
||||||
const insertText = ` ${expectedLog}\n`;
|
const insertPos = new vscode.Position(diagnostic.range.start.line + 1, 0);
|
||||||
fix.edit.insert(document.uri, new vscode.Position(diagnostic.range.start.line + 1, 0), insertText);
|
// 8 Leerzeichen Einrückung für den Body-Inhalt
|
||||||
|
action.edit.insert(document.uri, insertPos, ` ${expectedLog}\n`);
|
||||||
fix.diagnostics = [diagnostic];
|
action.diagnostics = [diagnostic];
|
||||||
fix.isPreferred = true;
|
action.isPreferred = true;
|
||||||
|
return [action];
|
||||||
return [fix];
|
}
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,20 +1,19 @@
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { TplQuickFix } from './TplQuickFix';
|
|
||||||
|
|
||||||
export class TabFix implements TplQuickFix {
|
export class TabFix {
|
||||||
public readonly diagnosticCode = 'invalid_tab';
|
public diagnosticCode = 'invalid_tab';
|
||||||
|
|
||||||
public provideFix(document: vscode.TextDocument, diagnostic: vscode.Diagnostic): vscode.CodeAction[] {
|
public provideFix(document: vscode.TextDocument, diagnostic: vscode.Diagnostic): vscode.CodeAction[] {
|
||||||
const fix = new vscode.CodeAction("Tabs in 4 Spaces umwandeln", vscode.CodeActionKind.QuickFix);
|
const action = new vscode.CodeAction("Convert tabs to 4 spaces", vscode.CodeActionKind.QuickFix);
|
||||||
fix.edit = new vscode.WorkspaceEdit();
|
action.edit = new vscode.WorkspaceEdit();
|
||||||
|
|
||||||
const line = document.lineAt(diagnostic.range.start.line);
|
const line = document.lineAt(diagnostic.range.start.line);
|
||||||
const newText = line.text.replace(/\t/g, ' ');
|
const newText = line.text.replace(/\t/g, ' ');
|
||||||
|
|
||||||
fix.edit.replace(document.uri, line.range, newText);
|
action.edit.replace(document.uri, line.range, newText);
|
||||||
fix.diagnostics = [diagnostic];
|
action.diagnostics = [diagnostic];
|
||||||
fix.isPreferred = true; // Setzt dies als Standard-Vorschlag
|
action.isPreferred = true;
|
||||||
|
|
||||||
return [fix];
|
return [action];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,36 +1,99 @@
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { CompletionStrategy } from './CompletionStrategy';
|
import { CompletionStrategy } from './CompletionStrategy';
|
||||||
import { tplBuiltIns } from '../../data/TplBuiltIns';
|
import { tplBuiltIns } from '../../data/TplBuiltIns';
|
||||||
import { generatedTplBuiltIns } from '../../data/GeneratedTplBuiltIns'; // Pfad ggf. anpassen
|
import { workspaceIndex } from '../../core/WorkspaceIndex';
|
||||||
|
|
||||||
const allTplFunctions: Record<string, any> = { ...tplBuiltIns, ...generatedTplBuiltIns };
|
|
||||||
|
|
||||||
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> {
|
public async provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): Promise<vscode.CompletionItem[] | undefined> {
|
||||||
const text = document.getText();
|
const text = document.getText();
|
||||||
const items: vscode.CompletionItem[] = [];
|
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) {
|
if (dotMatch) {
|
||||||
const prefix = dotMatch[1];
|
const prefix = dotMatch[1];
|
||||||
|
|
||||||
// Auf erweitertes Dictionary prüfen
|
// BMC Built-ins ignorieren (übernimmt BuiltInCompletion)
|
||||||
const bmcNamespaces = Object.keys(allTplFunctions);
|
const bmcNamespaces = Object.keys(tplBuiltIns);
|
||||||
if (bmcNamespaces.includes(prefix)) {
|
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 importRegex = new RegExp(`from\\s+([a-zA-Z0-9_.]+)\\s+import\\s+${prefix}\\b`);
|
||||||
const match = importRegex.exec(text);
|
const match = importRegex.exec(text);
|
||||||
if (match) {
|
if (match) {
|
||||||
const moduleName = match[1];
|
const moduleName = match[1];
|
||||||
const externalItems = await this.resolveImportedMethods(moduleName, prefix);
|
// Filtere Funktionen aus dem importierten Modul, die zu diesem Prefix gehören
|
||||||
return externalItems;
|
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');
|
const lines = text.split('\n');
|
||||||
let currentDefBlock = "";
|
let currentDefBlock = "";
|
||||||
|
|
||||||
|
|
@ -40,17 +103,26 @@ export class LocalContextCompletion implements CompletionStrategy {
|
||||||
if (defBlockMatch) { currentDefBlock = defBlockMatch[1]; continue; }
|
if (defBlockMatch) { currentDefBlock = defBlockMatch[1]; continue; }
|
||||||
if (line.match(/^\s*end\s+definitions?/)) { currentDefBlock = ""; 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) {
|
if (defineMatch) {
|
||||||
const funcName = defineMatch[1];
|
const funcName = defineMatch[1];
|
||||||
const fullName = currentDefBlock ? `${currentDefBlock}.${funcName}` : funcName;
|
const fullName = currentDefBlock ? `${currentDefBlock}.${funcName}` : funcName;
|
||||||
const item = new vscode.CompletionItem(fullName, vscode.CompletionItemKind.Function);
|
const item = new vscode.CompletionItem(fullName, vscode.CompletionItemKind.Function);
|
||||||
item.detail = currentDefBlock ? `Local Function in '${currentDefBlock}'` : `Local 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);
|
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;
|
const fromImportRegex = /from\s+([a-zA-Z0-9_.]+)\s+import\s+([a-zA-Z0-9_]+)/g;
|
||||||
let matchVar;
|
let matchVar;
|
||||||
while ((matchVar = fromImportRegex.exec(text)) !== null) {
|
while ((matchVar = fromImportRegex.exec(text)) !== null) {
|
||||||
|
|
@ -59,6 +131,7 @@ export class LocalContextCompletion implements CompletionStrategy {
|
||||||
items.push(item);
|
items.push(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lokale Variablen (:=) vorschlagen
|
||||||
const varRegex = /([a-zA-Z0-9_]+)\s*:=/g;
|
const varRegex = /([a-zA-Z0-9_]+)\s*:=/g;
|
||||||
const foundVars = new Set<string>();
|
const foundVars = new Set<string>();
|
||||||
while ((matchVar = varRegex.exec(text)) !== null) {
|
while ((matchVar = varRegex.exec(text)) !== null) {
|
||||||
|
|
@ -70,37 +143,4 @@ export class LocalContextCompletion implements CompletionStrategy {
|
||||||
|
|
||||||
return items;
|
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';
|
import { LogFix } from '../../providers/actions/LogFix';
|
||||||
|
|
||||||
suite('Code Actions Test Suite', () => {
|
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 fix = new TabFix();
|
||||||
const diag = new vscode.Diagnostic(new vscode.Range(0, 0, 0, 1), "Tab found", vscode.DiagnosticSeverity.Warning);
|
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 = {
|
const mockDoc = {
|
||||||
uri: vscode.Uri.file('test.tpl'),
|
uri: vscode.Uri.file('test.tpl'),
|
||||||
lineAt: () => ({ text: '\t\tcode', range: new vscode.Range(0, 0, 0, 6) })
|
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);
|
const actions = fix.provideFix(mockDoc, diag);
|
||||||
assert.strictEqual(actions.length, 1);
|
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);
|
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 fix = new LogFix();
|
||||||
const diag = new vscode.Diagnostic(new vscode.Range(10, 0, 10, 4), "Missing log");
|
const diag = new vscode.Diagnostic(new vscode.Range(10, 0, 10, 4), "Missing log");
|
||||||
diag.source = 'log.debug("Start pattern Test");'; // Linter-Source Simulation
|
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);
|
const actions = fix.provideFix(mockDoc, diag);
|
||||||
assert.strictEqual(actions.length, 1);
|
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 assert from 'assert';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { BuiltInCompletion } from '../../providers/completions/BuiltInCompletion';
|
|
||||||
import { SnippetCompletion } from '../../providers/completions/SnippetCompletion';
|
import { SnippetCompletion } from '../../providers/completions/SnippetCompletion';
|
||||||
import { LocalContextCompletion } from '../../providers/completions/LocalContextCompletion';
|
import { LocalContextCompletion } from '../../providers/completions/LocalContextCompletion';
|
||||||
|
import { workspaceIndex } from '../../core/WorkspaceIndex';
|
||||||
|
|
||||||
// Hilfsfunktion simuliert ein vscode.TextDocument
|
|
||||||
function createMockDoc(text: string): any {
|
function createMockDoc(text: string): any {
|
||||||
const lines = text.split('\n');
|
const lines = text.split('\n');
|
||||||
return {
|
return {
|
||||||
|
|
@ -14,51 +13,68 @@ function createMockDoc(text: string): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
suite('Completion Provider Test Suite', () => {
|
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
|
// Feeds the RAM index with a dummy file before tests
|
||||||
const docText = `pattern Test 1.0\nmetadata\n `;
|
suiteSetup(() => {
|
||||||
const mockDoc = createMockDoc(docText);
|
const dummyUri = vscode.Uri.file('/fake/path/Company.tools.tpl');
|
||||||
const position = new vscode.Position(2, 4); // Zeile 2, Cursor am Ende
|
const dummyText = `
|
||||||
|
tpl 1.23 module Company.tools;
|
||||||
const result = await strategy.provideCompletions(mockDoc, position, ' ');
|
definitions network_helpers
|
||||||
|
define ping(host, ip)
|
||||||
assert.ok(result !== undefined);
|
end define;
|
||||||
const hasIf = result!.some(item => item.label === 'if');
|
end definitions;
|
||||||
assert.strictEqual(hasIf, false, "Darf kein 'if' im metadata Block vorschlagen");
|
`.trim();
|
||||||
|
workspaceIndex.updateFile(dummyUri, dummyText);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('SnippetCompletion: Vorschläge im Body korrekt', async () => {
|
test('LocalContextCompletion: "from " suggests only RAM modules', 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 () => {
|
|
||||||
const strategy = new LocalContextCompletion();
|
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 result = await strategy.provideCompletions(mockDoc, position, 'from ');
|
||||||
const mockDoc = createMockDoc(docText);
|
|
||||||
const position = new vscode.Position(4, 0);
|
|
||||||
|
|
||||||
const result = await strategy.provideCompletions(mockDoc, position, 'do');
|
|
||||||
|
|
||||||
assert.ok(result !== undefined);
|
assert.ok(result !== undefined);
|
||||||
const funcItem = result!.find(item => item.label === 'my_tools.doWork');
|
const hasModule = result!.some(item => item.label === 'Company.tools');
|
||||||
assert.ok(funcItem !== undefined, "Muss die Methode inkl. Namespace extrahieren");
|
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;
|
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';
|
import { AlignmentRules } from '../../providers/formatters/AlignmentRules';
|
||||||
|
|
||||||
suite('Formatter Test Suite', () => {
|
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 indentRules = new IndentationRules();
|
||||||
const state = new FormatState();
|
const state = new FormatState();
|
||||||
state.tabSize = 4;
|
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);
|
let result = indentRules.apply('if x = 1 then', state);
|
||||||
assert.strictEqual(result, 'if x = 1 then');
|
assert.strictEqual(result, 'if x = 1 then');
|
||||||
assert.strictEqual(state.indentLevel, 1);
|
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);
|
result = indentRules.apply('log.debug("test");', state);
|
||||||
assert.strictEqual(result, ' log.debug("test");');
|
assert.strictEqual(result, ' log.debug("test");');
|
||||||
assert.strictEqual(state.indentLevel, 1);
|
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);
|
result = indentRules.apply('end if;', state);
|
||||||
assert.strictEqual(result, 'end if;');
|
assert.strictEqual(result, 'end if;');
|
||||||
assert.strictEqual(state.indentLevel, 0);
|
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 indentRules = new IndentationRules();
|
||||||
const state = new FormatState();
|
const state = new FormatState();
|
||||||
state.tabSize = 4;
|
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);
|
let result = indentRules.apply('elif x = 2 then', state);
|
||||||
assert.strictEqual(result, 'elif x = 2 then');
|
assert.strictEqual(result, 'elif x = 2 then');
|
||||||
assert.strictEqual(state.indentLevel, 1);
|
assert.strictEqual(state.indentLevel, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('AlignmentRules: Visual Alignment an runden Klammern', () => {
|
test('AlignmentRules: Visual alignment at parentheses', () => {
|
||||||
const alignRules = new AlignmentRules();
|
const alignRules = new AlignmentRules();
|
||||||
const state = new FormatState();
|
const state = new FormatState();
|
||||||
|
|
||||||
// 1. Zeile mit öffnender Klammer
|
// 1. Line with opening parenthesis
|
||||||
alignRules.apply('discovery.runCommand(', 'discovery.runCommand(', state);
|
alignRules.apply('discovery.runCommand(', 'discovery.runCommand(', state);
|
||||||
assert.strictEqual(state.parenAlignments.length, 1);
|
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);
|
const result = alignRules.apply('host,', 'host,', state);
|
||||||
assert.strictEqual(result, ' host,');
|
assert.strictEqual(result, ' host,');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,73 +3,82 @@ 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';
|
import { BlockRule } from '../../core/rules/BlockRule';
|
||||||
|
import { validateTplDocument } from '../../core/TplLinter';
|
||||||
|
|
||||||
suite('Linter Test Suite', () => {
|
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 rule = new SyntaxRule();
|
||||||
const ctx = new LinterContext();
|
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");';
|
const rawLine = ' log.debug("https://example.com/api?q=1//test");';
|
||||||
rule.checkLine(rawLine.trim(), rawLine, 0, ctx, [rawLine]);
|
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);
|
assert.strictEqual(ctx.diagnostics.length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('SyntaxRule: Erfasst TODOs als Information', () => {
|
test('Linter: Captures TODOs as Information', () => {
|
||||||
const rule = new SyntaxRule();
|
const mockDoc = {
|
||||||
const ctx = new LinterContext();
|
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';
|
const collection = vscode.languages.createDiagnosticCollection('test-collection');
|
||||||
rule.checkLine(rawLine.trim(), rawLine, 0, ctx, [rawLine]);
|
validateTplDocument(mockDoc, collection);
|
||||||
|
|
||||||
assert.strictEqual(ctx.diagnostics.length, 1);
|
const diags = collection.get(mockDoc.uri);
|
||||||
assert.strictEqual(ctx.diagnostics[0].severity, vscode.DiagnosticSeverity.Information);
|
assert.strictEqual(diags?.length, 1, "Must find exactly one TODO");
|
||||||
assert.ok(ctx.diagnostics[0].message.includes("TODO: Diese Funktion refactoren"));
|
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 rule = new SyntaxRule();
|
||||||
const ctx = new LinterContext();
|
const ctx = new LinterContext();
|
||||||
ctx.activePhaseName = 'body';
|
ctx.activePhaseName = 'body';
|
||||||
|
|
||||||
// Zeile 1: or-Umbruch
|
// Line 1: or-break
|
||||||
rule.checkLine('if x = 1 or', 'if x = 1 or', 0, ctx, []);
|
rule.checkLine('if x = 1 or', 'if x = 1 or', 0, ctx, []);
|
||||||
assert.strictEqual(ctx.diagnostics.length, 0);
|
assert.strictEqual(ctx.diagnostics.length, 0);
|
||||||
assert.strictEqual(ctx.inConditionBlock, true);
|
assert.strictEqual(ctx.inConditionBlock, true);
|
||||||
|
|
||||||
// Zeile 2: then-Abschluss
|
// Line 2: then-closure
|
||||||
rule.checkLine('y = 2 then', 'y = 2 then', 1, ctx, []);
|
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);
|
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 rule = new SyntaxRule();
|
||||||
const ctx = new LinterContext();
|
const ctx = new LinterContext();
|
||||||
ctx.inModuleMetadata = true; // Wir sind im Modul-Metadata
|
ctx.inModuleMetadata = true; // We are in module metadata
|
||||||
|
|
||||||
const rawLine = " origin := 'Custom';";
|
const rawLine = " origin := 'Custom';";
|
||||||
rule.checkLine(rawLine.trim(), rawLine, 0, ctx, [rawLine]);
|
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', () => {
|
test('BlockRule: if/for/while stack tracking', () => {
|
||||||
const rule = new BlockRule();
|
const mockDoc = {
|
||||||
const ctx = new LinterContext();
|
languageId: 'tpl',
|
||||||
ctx.hasModuleDef = true;
|
getText: () => "tpl 1.0 module Test;\nif condition then\nend for;",
|
||||||
ctx.inBlock = 'pattern';
|
uri: vscode.Uri.file('test.tpl')
|
||||||
|
} as any;
|
||||||
|
|
||||||
// Öffnet Block
|
const collection = vscode.languages.createDiagnosticCollection('test-collection-blockrule');
|
||||||
rule.checkLine('if true then', 'if true then', 0, ctx, []);
|
validateTplDocument(mockDoc, collection);
|
||||||
assert.strictEqual(ctx.openControlBlocks.length, 1);
|
|
||||||
assert.strictEqual(ctx.openControlBlocks[0].type, 'if');
|
|
||||||
|
|
||||||
// Schließt falschen Block (end for statt end if)
|
const diags = collection.get(mockDoc.uri);
|
||||||
rule.checkLine('end for;', 'end for;', 1, ctx, []);
|
|
||||||
assert.strictEqual(ctx.diagnostics.length, 1);
|
// We check language-agnostically: Is there an Error exactly on line 2 (the 'end for;')?
|
||||||
assert.ok(ctx.diagnostics[0].message.includes("Falsches Ende"));
|
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