Fixes for common mistakes the current add on had

This commit is contained in:
kuro 2026-10-28 22:04:56 +01:00
parent 1c160c3e1b
commit 3b597e6df4
15 changed files with 7899 additions and 7600 deletions

View File

@ -50,7 +50,7 @@
"tpl.metadata.publisher": {
"type": "string",
"default": "IT-Operations",
"description": "Standard-Publisher ffor new generated TPL-Modules."
"description": "Standard-Publisher for new generated TPL-Modules."
},
"tpl.metadata.treePathBase": {
"type": "string",
@ -90,13 +90,18 @@
"tpl.metadata.treePath.staticNodes": {
"type": "array",
"items": { "type": "string" },
"default": ["default"],
"default": [""],
"description": "Fixed definitions for the tree_path left to right (e.g. ['Company', 'Software']). The rest of the levels are free to choose."
},
"tpl.version": {
"type": "number",
"default": 23,
"description": "Aktuelle TPL-Nebenversion (z. B. 23 für TPL 1.23). Wird genutzt um veraltete Module zu erkennen und beim Generieren eingefügt."
"description": "Current TPL-Subversion (e. g. 23 for TPL 1.23). Used to find old Modules and for auto generating the sceleton."
},
"tpl.rules.requireBodyLog": {
"type": "boolean",
"default": true,
"description": "Require a log.debug statement at the start of a pattern's body phase."
}
},
"commands": [

View File

@ -4,6 +4,7 @@ import { SyntaxRule } from './rules/SyntaxRule';
import { BlockRule } from './rules/BlockRule';
import { MetadataRule } from './rules/MetadataRule';
import { IndentationRule } from './rules/IndentationRule';
import { ParameterRule } from './rules/ParameterRule';
export function validateTplDocument(document: vscode.TextDocument, collection: vscode.DiagnosticCollection) {
if (document.languageId !== 'tpl') {return;}
@ -13,10 +14,11 @@ export function validateTplDocument(document: vscode.TextDocument, collection: v
const ctx = new LinterContext();
const rules = [
new IndentationRule(),
new SyntaxRule(),
new MetadataRule(),
new BlockRule(),
new IndentationRule()
new ParameterRule()
];
for (let i = 0; i < lines.length; i++) {

41
src/core/TplLookup.ts Normal file
View File

@ -0,0 +1,41 @@
import { tplBuiltIns } from '../data/TplBuiltIns';
import { generatedTplBuiltIns } from '../data/GeneratedTplBuiltIns';
export class TplLookup {
// Holt eine spezifische Methode (Case-Insensitive)
public static getBuiltInFunction(namespace: string, funcName: string): any | null {
const nsLower = namespace.toLowerCase();
const fnLower = funcName.toLowerCase();
// 1. Suche in Core Built-Ins (discovery, model, etc.)
const coreKey = Object.keys(tplBuiltIns).find(k => k.toLowerCase() === nsLower);
if (coreKey) {
const methods = (tplBuiltIns as any)[coreKey];
const mKey = Object.keys(methods).find(k => k.toLowerCase() === fnLower);
if (mKey) return methods[mKey];
}
// 2. Suche in Generierten TKU Built-Ins (rdbms_functions, etc.)
const tkuKey = Object.keys(generatedTplBuiltIns).find(k => k.toLowerCase() === nsLower);
if (tkuKey) {
const methods = (generatedTplBuiltIns as any)[tkuKey];
const mKey = Object.keys(methods).find(k => k.toLowerCase() === fnLower);
if (mKey) return methods[mKey];
}
return null;
}
// Holt alle Methoden eines Moduls für die Autovervollständigung (Case-Insensitive)
public static getAllMethodsForNamespace(namespace: string): Record<string, any> | null {
const nsLower = namespace.toLowerCase();
const coreKey = Object.keys(tplBuiltIns).find(k => k.toLowerCase() === nsLower);
if (coreKey) return (tplBuiltIns as any)[coreKey];
const tkuKey = Object.keys(generatedTplBuiltIns).find(k => k.toLowerCase() === nsLower);
if (tkuKey) return (generatedTplBuiltIns as any)[tkuKey];
return null;
}
}

View File

@ -1,20 +1,18 @@
import * as vscode from 'vscode';
export interface TplFunctionSignature {
name: string; // z.B. "modelling.doWork"
moduleName: string; // z.B. "Company.software_modelling"
params: string[]; // z.B. ["host", "ip_address"]
docString?: string; // Extrahierter Kommentar
name: string;
moduleName: string;
params: string[];
docString?: string;
hasReturnValue?: boolean;
lineIdx: number; // NEU: Für 'Go to Definition'
fsPath: string; // NEU: Für 'Go to Definition'
}
export class WorkspaceIndex {
// Map: Datei-Pfad -> Map von Funktionen (Funktionsname -> Signatur)
private fileIndex = new Map<string, Map<string, TplFunctionSignature>>();
// Map: Datei-Pfad -> Modulname
private fileModules = new Map<string, string>();
// Map: Modulname -> Set von Definitions-Blöcken (für die Autovervollständigung beim Import)
private moduleDefinitions = new Map<string, Set<string>>();
public updateFile(uri: vscode.Uri, text: string) {
@ -27,22 +25,19 @@ export class WorkspaceIndex {
const functions = new Map<string, TplFunctionSignature>();
const defBlocks = new Set<string>();
// 1. Modulnamen parsen
const modMatch = text.match(/tpl\s+1\.\d+\s+module\s+([a-zA-Z0-9_.]+)\s*;/);
if (modMatch) {
currentModule = modMatch[1];
}
this.fileModules.set(fsPath, currentModule);
// 2. Blöcke und Funktionen parsen
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Startet ein Definitions-Block?
const defBlockMatch = line.match(/^\s*definitions?\s+([a-zA-Z0-9_]+)/);
if (defBlockMatch) {
currentDefBlock = defBlockMatch[1];
defBlocks.add(currentDefBlock); // Block merken!
defBlocks.add(currentDefBlock);
continue;
}
if (line.match(/^\s*end\s+definitions?/)) {
@ -50,38 +45,44 @@ export class WorkspaceIndex {
continue;
}
// Patterns, Syncmappings und Tabellen für den Import freigeben
const structMatch = line.match(/^\s*(?:pattern|syncmapping|table)\s+([a-zA-Z0-9_]+)/);
if (structMatch) {
defBlocks.add(structMatch[1]);
}
// Sucht nach 'define name(param1, param2)'
const defineMatch = line.match(/^\s*define\s+([a-zA-Z0-9_]+)\b(?:\s*\((.*?)\))?/);
if (defineMatch) {
const funcName = defineMatch[1];
const paramString = defineMatch[2] || "";
const fullName = currentDefBlock ? `${currentDefBlock}.${funcName}` : funcName;
// Parameter säubern und zählen
const params = paramString.split(',').map(p => p.trim()).filter(p => p.length > 0);
// DocString extrahieren
let docString = undefined;
const textAfterDefine = text.substring(text.indexOf(line) + line.length);
const docMatch = textAfterDefine.match(/^\s*(?:"""|''')([\s\S]*?)(?:"""|''')/);
if (docMatch) { docString = docMatch[1].trim(); }
let hasReturnValue = false;
for (let j = i + 1; j < lines.length; j++) {
if (lines[j].match(/^\s*end\s+define/)) break;
if (lines[j].match(/^\s*return\b/)) {
hasReturnValue = true;
break;
}
}
functions.set(fullName, {
name: fullName,
moduleName: currentModule,
params: params,
docString: docString
docString: docString,
hasReturnValue: hasReturnValue,
lineIdx: i,
fsPath: fsPath
});
}
}
// In den RAM schreiben
this.fileIndex.set(fsPath, functions);
if (currentModule) {
this.moduleDefinitions.set(currentModule, defBlocks);
@ -97,20 +98,15 @@ export class WorkspaceIndex {
this.fileModules.delete(uri.fsPath);
}
// --- HILFSFUNKTIONEN FÜR PROVIDER UND LINTER ---
// Liefert alle Module (z.B. für "from [Autocomplete] import ...")
public getAllModules(): string[] {
return Array.from(this.moduleDefinitions.keys());
}
// Liefert alle Definitions-Blöcke eines Moduls (z.B. für "from XYZ import [Autocomplete]")
public getDefinitionBlocksForModule(moduleName: string): string[] {
const blocks = this.moduleDefinitions.get(moduleName);
return blocks ? Array.from(blocks) : [];
}
// Liefert alle Funktionen eines Moduls (für "modelling.[Autocomplete]")
public getFunctionsForModule(moduleName: string): TplFunctionSignature[] {
const result: TplFunctionSignature[] = [];
for (const [fsPath, map] of this.fileIndex.entries()) {
@ -120,6 +116,18 @@ export class WorkspaceIndex {
}
return result;
}
public getFunctionSignatures(funcName: string): TplFunctionSignature[] {
const result: TplFunctionSignature[] = [];
for (const map of this.fileIndex.values()) {
for (const sig of map.values()) {
if (sig.name === funcName || sig.name.endsWith(`.${funcName}`)) {
result.push(sig);
}
}
}
return result;
}
}
export const workspaceIndex = new WorkspaceIndex();

View File

@ -7,17 +7,17 @@ export class BlockRule implements LinterRule {
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void {
if (!ctx.hasModuleDef) {
if (line.startsWith('tpl ')) { ctx.hasModuleDef = true; }
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Every file has to start with 'tpl 1.x module <name>;'", vscode.DiagnosticSeverity.Error); ctx.hasModuleDef = true; }
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Every file must start with 'tpl 1.x module <name>;'.", vscode.DiagnosticSeverity.Error); ctx.hasModuleDef = true; }
}
// Wenn wir im Metadata-Block sind, ignorieren wir den Rest. Die MetadataRule übernimmt!
// If we are inside the metadata block, we ignore the rest. MetadataRule handles it!
if (ctx.inModuleMetadata || (line.startsWith('metadata') && ctx.inBlock === 'none')) {
return;
}
if (line.match(/^(pattern|syncmapping|table|definitions?|enumeration|configuration)\b/)) {
if (!ctx.hasModuleMetadata && ctx.inBlock === 'none') {
ctx.addDiag(lineIdx, 0, line.length, "A 'metadata' block on modul-level is needed before any other blocks can be defined.", vscode.DiagnosticSeverity.Error);
ctx.addDiag(lineIdx, 0, line.length, "A 'metadata' block at the module level is mandatory before defining other blocks.", vscode.DiagnosticSeverity.Error);
}
}
@ -33,10 +33,10 @@ export class BlockRule implements LinterRule {
public checkEnd(lines: string[], ctx: LinterContext): void {
for (const unclosedLine of ctx.openDefines) {
ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Block 'define' was not ended with 'end define;'!", vscode.DiagnosticSeverity.Error);
ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Block 'define' was not closed with 'end define;'!", vscode.DiagnosticSeverity.Error);
}
for (const block of ctx.openControlBlocks) {
ctx.addDiag(block.lineIdx, 0, lines[block.lineIdx].length, `Block '${block.type}' was not ended with 'end${block.type};'!`, vscode.DiagnosticSeverity.Error);
ctx.addDiag(block.lineIdx, 0, lines[block.lineIdx].length, `Block '${block.type}' was not closed with 'end ${block.type};'!`, vscode.DiagnosticSeverity.Error);
}
}
@ -52,7 +52,7 @@ export class BlockRule implements LinterRule {
private closeControlBlock(expectedType: string, line: string, lineIdx: number, ctx: LinterContext) {
if (ctx.openControlBlocks.length === 0) {
ctx.addDiag(lineIdx, 0, line.length, `'end ${expectedType}' found without open block.`, vscode.DiagnosticSeverity.Error);
ctx.addDiag(lineIdx, 0, line.length, `Found 'end ${expectedType}' without an open block.`, vscode.DiagnosticSeverity.Error);
return;
}
@ -65,20 +65,20 @@ export class BlockRule implements LinterRule {
private handleBlockOpenings(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
if (line.startsWith('definitions ') || line.startsWith('definition ')) {
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "A defintions block has to be declared on the highest level.", vscode.DiagnosticSeverity.Error); }
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "A 'definitions' block must be declared at the top level.", vscode.DiagnosticSeverity.Error); }
ctx.inBlock = 'definitions';
}
else if (line.startsWith('define ')) { ctx.openDefines.push(lineIdx); }
else if (line.startsWith('enumeration ')) {
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "A enumeration has to be declared on the highest level.", vscode.DiagnosticSeverity.Error); }
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "An 'enumeration' must be declared at the top level.", vscode.DiagnosticSeverity.Error); }
ctx.inBlock = 'enumeration';
}
else if (line.startsWith('configuration ')) {
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "A configuration has to be declared on the highest level.", vscode.DiagnosticSeverity.Error); }
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "A 'configuration' must be declared at the top level.", vscode.DiagnosticSeverity.Error); }
ctx.inBlock = 'configuration';
}
else if (line.startsWith('pattern ') || line.startsWith('syncmapping ')) {
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, 7, "A Pattern/Syncmapping can not be declared inside another block.", vscode.DiagnosticSeverity.Error); }
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, 7, "A pattern or syncmapping cannot be declared inside another block.", vscode.DiagnosticSeverity.Error); }
ctx.inBlock = 'pattern';
ctx.currentPatternPhase = PatternPhase.None;
ctx.activePhaseName = "";
@ -86,7 +86,7 @@ export class BlockRule implements LinterRule {
const nameMatch = line.match(/(pattern|syncmapping)\s+([a-zA-Z0-9_]+)\s+\d+\.\d+/);
if (nameMatch) { ctx.currentPatternName = nameMatch[2]; }
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Invalid Pattern declaration. Expected: 'pattern <Name> <Version>'", vscode.DiagnosticSeverity.Error); }
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Invalid pattern declaration. Expected: 'pattern <Name> <Version>'", vscode.DiagnosticSeverity.Error); }
}
else if (line.startsWith('table ')) {
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, 5, "A table cannot be declared inside another block.", vscode.DiagnosticSeverity.Error); }
@ -97,8 +97,8 @@ export class BlockRule implements LinterRule {
private checkPatternPhases(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]) {
const checkPhase = (phaseName: string, phaseEnum: PatternPhase) => {
if (line.startsWith(phaseName)) {
if (ctx.activePhaseName !== "") { ctx.addDiag(ctx.activePhaseLine, 0, ctx.activePhaseName.length, `Block '${ctx.activePhaseName}' wurde nicht mit 'end${ctx.activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); }
if (ctx.currentPatternPhase >= phaseEnum) { ctx.addDiag(lineIdx, 0, phaseName.length, `'${phaseName}' Block ist hier nicht erlaubt.`, vscode.DiagnosticSeverity.Error); }
if (ctx.activePhaseName !== "") { ctx.addDiag(ctx.activePhaseLine, 0, ctx.activePhaseName.length, `Block '${ctx.activePhaseName}' was not closed with 'end ${ctx.activePhaseName};'.`, vscode.DiagnosticSeverity.Error); }
if (ctx.currentPatternPhase >= phaseEnum) { ctx.addDiag(lineIdx, 0, phaseName.length, `Block '${phaseName}' is not allowed here.`, vscode.DiagnosticSeverity.Error); }
ctx.currentPatternPhase = phaseEnum;
ctx.activePhaseName = phaseName;
@ -114,10 +114,14 @@ export class BlockRule implements LinterRule {
checkPhase('removal', PatternPhase.Removal);
if (line.startsWith('body')) {
if (ctx.activePhaseName !== "") { ctx.addDiag(ctx.activePhaseLine, 0, ctx.activePhaseName.length, `Block '${ctx.activePhaseName}' wurde nicht mit 'end${ctx.activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); }
if (ctx.activePhaseName !== "") { ctx.addDiag(ctx.activePhaseLine, 0, ctx.activePhaseName.length, `Block '${ctx.activePhaseName}' was not closed with 'end ${ctx.activePhaseName};'.`, vscode.DiagnosticSeverity.Error); }
ctx.activePhaseName = "body";
ctx.activePhaseLine = lineIdx;
const config = vscode.workspace.getConfiguration('tpl');
const requireLog = config.get<boolean>('rules.requireBodyLog', true);
if (requireLog) {
let foundLog = false;
for (let j = lineIdx + 1; j < lines.length; j++) {
const aheadLine = lines[j].trim();
@ -127,17 +131,18 @@ export class BlockRule implements LinterRule {
if (!foundLog) {
const expectedLog = `log.debug("Start pattern ${ctx.currentPatternName}");`;
const diag = ctx.addDiag(lineIdx, 0, rawLine.length, `Inside the Body the Log-Statement is missing, e.g. Pattern-Namen: ${expectedLog}`, vscode.DiagnosticSeverity.Error);
const diag = ctx.addDiag(lineIdx, 0, rawLine.length, `Missing log statement with pattern name in body. E.g.: ${expectedLog}`, vscode.DiagnosticSeverity.Error);
diag.code = 'missing_log_start';
diag.source = expectedLog;
}
}
}
const closers = ['end metadata', 'end overview', 'end constants', 'end triggers', 'end mapping', 'end removal', 'end body'];
for (const closer of closers) {
if (line.startsWith(closer)) {
const phase = closer.replace('end ', '').replace(';', '').trim();
if (ctx.activePhaseName !== phase) { ctx.addDiag(lineIdx, 0, line.length, `Unerwartetes '${closer}'. Erwartet wurde 'end ${ctx.activePhaseName};'.`, vscode.DiagnosticSeverity.Error); }
if (ctx.activePhaseName !== phase) { ctx.addDiag(lineIdx, 0, line.length, `Unexpected '${closer}'. Expected 'end ${ctx.activePhaseName};'.`, vscode.DiagnosticSeverity.Error); }
else { ctx.activePhaseName = ""; }
}
}
@ -145,20 +150,20 @@ export class BlockRule implements LinterRule {
private checkEndBlocks(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
if (line.startsWith('end pattern') || line.startsWith('end syncmapping')) {
if (ctx.activePhaseName !== "") { ctx.addDiag(ctx.activePhaseLine, 0, ctx.activePhaseName.length, `Block '${ctx.activePhaseName}' wurde nicht mit 'end${ctx.activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); }
if (ctx.inBlock !== 'pattern') { ctx.addDiag(lineIdx, 0, 11, "'end pattern' found without open pattern block.", vscode.DiagnosticSeverity.Error); }
if (ctx.activePhaseName !== "") { ctx.addDiag(ctx.activePhaseLine, 0, ctx.activePhaseName.length, `Block '${ctx.activePhaseName}' was not closed with 'end ${ctx.activePhaseName};'.`, vscode.DiagnosticSeverity.Error); }
if (ctx.inBlock !== 'pattern') { ctx.addDiag(lineIdx, 0, 11, "Found 'end pattern' without an open pattern block.", vscode.DiagnosticSeverity.Error); }
ctx.inBlock = 'none';
}
else if (line.startsWith('end table') || line.startsWith('end enumeration') || line.startsWith('end configuration')) {
ctx.inBlock = 'none';
}
else if (line.startsWith('end definitions') || line.startsWith('end definition')) {
if (ctx.inBlock !== 'definitions') { ctx.addDiag(lineIdx, 0, line.length, "'end definitions' found without open definitions block.", vscode.DiagnosticSeverity.Error); }
if (ctx.inBlock !== 'definitions') { ctx.addDiag(lineIdx, 0, line.length, "Found 'end definitions' without an open definitions block.", vscode.DiagnosticSeverity.Error); }
ctx.inBlock = 'none';
}
else if (line.startsWith('end define')) {
if (ctx.openDefines.length > 0) { ctx.openDefines.pop(); }
else { ctx.addDiag(lineIdx, 0, line.length, "'end define' found without open define.", vscode.DiagnosticSeverity.Error); }
else { ctx.addDiag(lineIdx, 0, line.length, "Found 'end define' without an open 'define'.", vscode.DiagnosticSeverity.Error); }
}
}
}

View File

@ -8,9 +8,9 @@ export class IndentationRule implements LinterRule {
if (rawLine.trim().length === 0) {return;}
if (rawLine.includes('\t')) {return;}
// HIER IST DER FIX: Wenn wir innerhalb von Klammern sind (Visual Alignment),
// Sobald wir in Klammern oder einem Multi-Line Import sind (Visual Alignment),
// greift die 4-Leerzeichen-Regel nicht!
if (ctx.parenStack.length > 0 || ctx.bracketStack.length > 0) {
if (ctx.parenStack.length > 0 || ctx.bracketStack.length > 0 || ctx.inImportBlock) {
return;
}

View File

@ -0,0 +1,157 @@
import * as vscode from 'vscode';
import { LinterRule } from './LinterRule';
import { LinterContext } from '../LinterContext';
import { workspaceIndex, TplFunctionSignature } from '../WorkspaceIndex';
import { TplLookup } from '../TplLookup'; // Nutzt ab sofort das zentrale Lexikon
export class ParameterRule implements LinterRule {
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void {
let cleanLine = "";
let inString = false;
let stringChar = '';
let inTripleQuote = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
// 1. Single-Line Kommentare (//) ignorieren
if (!inString && !inTripleQuote && char === '/' && line[i+1] === '/') {
break;
}
// 2. TPL Triple Quotes (""" oder ''') erkennen
if (!inString && !inTripleQuote &&
(char === '"' || char === "'") &&
line[i+1] === char &&
line[i+2] === char) {
inTripleQuote = true;
stringChar = char;
cleanLine += char + char + char; // Die """ behalten, damit wir die Zeile nicht komplett leeren
i += 2; // Überspringe die nächsten zwei Zeichen
continue;
}
// 3. Triple Quotes beenden
if (inTripleQuote) {
if (char === stringChar && line[i+1] === stringChar && line[i+2] === stringChar) {
inTripleQuote = false;
cleanLine += char + char + char;
i += 2;
}
continue; // Alles innerhalb von """ ignorieren!
}
// 4. Normale Strings beenden
if (inString) {
if (char === '\\') {
i++; // Escape-Sequenzen ignorieren
} else if (char === stringChar) {
inString = false;
cleanLine += char;
}
continue; // Alles innerhalb von " ignorieren!
}
// 5. Normale Strings starten
if (!inString && !inTripleQuote && (char === '"' || char === "'")) {
inString = true;
stringChar = char;
cleanLine += char;
continue;
}
// Wenn wir hier ankommen, sind wir im echten, ausführbaren TPL-Code
cleanLine += char;
}
if (cleanLine.trim().length === 0) return;
// Die Definition der Funktion selbst ist kein Aufruf und wird ignoriert.
if (/^\s*define\s+/.test(cleanLine)) return;
const callRegex = /\b([a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)?)\s*\(/g;
let match;
while ((match = callRegex.exec(cleanLine)) !== null) {
const funcName = match[1];
const callStart = match.index;
const parenStart = callStart + funcName.length;
if (/^(if|elif|where|for|while|in|matches|has|subword|defined)\b/.test(funcName)) continue;
let depth = 0;
let parenEnd = -1;
for (let i = parenStart; i < cleanLine.length; i++) {
if (cleanLine[i] === '(') depth++;
else if (cleanLine[i] === ')') {
depth--;
if (depth === 0) {
parenEnd = i;
break;
}
}
}
if (parenEnd !== -1) {
const paramsStr = cleanLine.substring(parenStart + 1, parenEnd).trim();
let paramCount = 0;
if (paramsStr.length > 0) {
let paramDepth = 0;
let commas = 0;
for (let i = 0; i < paramsStr.length; i++) {
if (paramsStr[i] === '(' || paramsStr[i] === '[') paramDepth++;
else if (paramsStr[i] === ')' || paramsStr[i] === ']') paramDepth--;
else if (paramsStr[i] === ',' && paramDepth === 0) commas++;
}
paramCount = commas + 1;
}
this.validateFunctionParams(funcName, paramCount, lineIdx, rawLine, ctx);
this.validateReturnValue(funcName, cleanLine, lineIdx, rawLine, ctx);
}
}
}
public checkEnd(lines: string[], ctx: LinterContext): void {
// empty
}
private validateFunctionParams(funcName: string, passedCount: number, lineIdx: number, rawLine: string, ctx: LinterContext) {
const parts = funcName.split('.');
if (parts.length > 1) {
// Case-Insensitive Prüfung über den neuen Lookup!
if (TplLookup.getBuiltInFunction(parts[0], parts[1])) {
return; // Funktion existiert, Linter ist zufrieden.
}
}
const signatures = workspaceIndex.getFunctionSignatures(funcName);
if (signatures.length > 0) {
const matches = signatures.some((sig: TplFunctionSignature) => sig.params.length === passedCount);
if (!matches) {
const expected = signatures[0].params.length;
const idx = rawLine.indexOf(funcName);
ctx.addDiag(lineIdx, Math.max(0, idx), Math.max(0, idx) + funcName.length, `Function '${funcName}' expects ${expected} parameter(s), but ${passedCount} were provided.`, vscode.DiagnosticSeverity.Warning);
}
} else if (parts.length > 1) {
const idx = rawLine.indexOf(funcName);
ctx.addDiag(lineIdx, Math.max(0, idx), Math.max(0, idx) + funcName.length, `Unknown function '${funcName}'. Please check your imports or contact the plugin provider.`, vscode.DiagnosticSeverity.Information);
}
}
private validateReturnValue(funcName: string, cleanLine: string, lineIdx: number, rawLine: string, ctx: LinterContext) {
const signatures = workspaceIndex.getFunctionSignatures(funcName);
if (signatures.length > 0 && signatures[0].hasReturnValue) {
const isAssigned = cleanLine.includes(':=');
const isUsedInCondition = /\b(if|elif|while)\b/.test(cleanLine);
if (!isAssigned && !isUsedInCondition) {
const idx = rawLine.indexOf(funcName);
ctx.addDiag(lineIdx, Math.max(0, idx), Math.max(0, idx) + funcName.length, `The return value of '${funcName}' is not used. Did you forget an assignment (:=)?`, vscode.DiagnosticSeverity.Warning);
}
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -4,7 +4,8 @@ import { TplCodeActionProvider } from './providers/TplCodeActionProvider';
import { TplDocumentFormatter } from './providers/TplDocumentFormatter';
import { TplCompletionProvider } from './providers/TplCompletionProvider';
import { TplHoverProvider } from './providers/TplHoverProvider';
import { workspaceIndex } from './core/WorkspaceIndex'; // <-- Index importiert
import { workspaceIndex } from './core/WorkspaceIndex';
import { TplDefinitionProvider } from './providers/TplDefinitionProvider';
export function activate(context: vscode.ExtensionContext) {
const diagnosticCollection = vscode.languages.createDiagnosticCollection('tpl-scanner');
@ -43,6 +44,7 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(vscode.languages.registerDocumentFormattingEditProvider('tpl', new TplDocumentFormatter()));
context.subscriptions.push(vscode.languages.registerCompletionItemProvider('tpl', new TplCompletionProvider(), '.'));
context.subscriptions.push(vscode.languages.registerHoverProvider('tpl', new TplHoverProvider()));
context.subscriptions.push(vscode.languages.registerDefinitionProvider('tpl', new TplDefinitionProvider()));
context.subscriptions.push(vscode.commands.registerCommand('tpl.scanWorkspace', () => {
scanWorkspace(diagnosticCollection);

View File

@ -0,0 +1,53 @@
import * as vscode from 'vscode';
import { workspaceIndex } from '../core/WorkspaceIndex';
export class TplDefinitionProvider implements vscode.DefinitionProvider {
public async provideDefinition(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Definition | null> {
const range = document.getWordRangeAtPosition(position, /[a-zA-Z0-9_.]+/);
if (!range) return null;
const word = document.getText(range);
const parts = word.split('.');
const funcName = parts.length > 1 ? parts[1] : word;
const text = document.getText();
// 1. LOKALE SUCHE: Ist die Funktion in der gleichen Datei?
const lines = text.split('\n');
for (let i = 0; i < lines.length; i++) {
const match = lines[i].match(new RegExp(`^\\s*define\\s+${funcName}\\b`));
if (match) {
// Wir haben sie lokal gefunden! Springe exakt dorthin.
return new vscode.Location(document.uri, new vscode.Position(i, lines[i].indexOf(funcName)));
}
}
// 2. GLOBALE SUCHE: Den RAM-Index nach anderen Modulen fragen
const signatures = workspaceIndex.getFunctionSignatures(funcName);
if (parts.length > 1) {
const prefix = parts[0];
// Wir schauen, ob es über from X import <prefix> importiert wurde
const importRegex = new RegExp(`from\\s+([a-zA-Z0-9_.]+)\\s+import\\s+[\\s\\S]*?\\b${prefix}\\b`);
const match = importRegex.exec(text);
if (match) {
const moduleName = match[1];
const targetFunc = signatures.find(sig => sig.moduleName === moduleName && sig.name.endsWith(`.${funcName}`));
if (targetFunc && targetFunc.fsPath) {
const uri = vscode.Uri.file(targetFunc.fsPath);
return new vscode.Location(uri, new vscode.Position(targetFunc.lineIdx, 0));
}
}
}
// 3. FALLBACK: Wenn es nicht als 'prefix.funktion' stand, aber eindeutig im Workspace ist
if (signatures.length > 0 && signatures[0].fsPath) {
const uri = vscode.Uri.file(signatures[0].fsPath);
return new vscode.Location(uri, new vscode.Position(signatures[0].lineIdx, 0));
}
// Wenn es eine Built-in Methode ist (wie discovery.runCommand), springt er nirgends hin
return null;
}
}

View File

@ -1,94 +1,38 @@
import * as vscode from 'vscode';
import { tplBuiltIns } from '../data/TplBuiltIns';
import { generatedTplBuiltIns } from '../data/GeneratedTplBuiltIns'; // Pfad ggf. anpassen
// Beide Datenquellen zusammenführen
const allTplFunctions: Record<string, any> = { ...tplBuiltIns, ...generatedTplBuiltIns };
import { TplLookup } from '../core/TplLookup';
import { workspaceIndex } from '../core/WorkspaceIndex';
export class TplHoverProvider implements vscode.HoverProvider {
public async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Hover | null> {
public provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): vscode.ProviderResult<vscode.Hover> {
const range = document.getWordRangeAtPosition(position, /[a-zA-Z0-9_.]+/);
if (!range) {return null;}
if (!range) return null;
const word = document.getText(range);
const text = document.getText();
const lines = text.split('\n');
const parts = word.split('.');
const importBase = parts[0];
if (parts.length > 1) {
// 1. Suche in Core & TKU Built-Ins (Case-Insensitive)
const funcData = TplLookup.getBuiltInFunction(parts[0], parts[1]);
if (funcData) {
const md = new vscode.MarkdownString();
md.appendCodeblock(`${parts[0]}.${parts[1]}(${funcData.snippets?.[0] || ''})`, 'tpl');
md.appendMarkdown(`---\n${funcData.description}`);
return new vscode.Hover(md);
}
}
// 2. Suche in lokalem Workspace (RAM Index)
const funcName = parts.length > 1 ? parts[1] : word;
// 1. BMC BUILT-INS & TKU DEFINES DYNAMISCH AUSLESEN
const bmcNamespaces = Object.keys(allTplFunctions);
if (bmcNamespaces.includes(importBase)) {
// Wenn die Methode in der Datei existiert, zeige die echte Doku!
if (funcName !== importBase && allTplFunctions[importBase][funcName]) {
const bmcDoc = allTplFunctions[importBase][funcName];
const signatures = workspaceIndex.getFunctionSignatures(funcName);
if (signatures.length > 0) {
const sig = signatures[0];
const md = new vscode.MarkdownString();
// Wir zeigen den echten Methodennamen als Code-Block
md.appendCodeblock(`${importBase}.${funcName}`, 'tpl');
md.appendMarkdown(`\n\n---\n\n${bmcDoc.description}`);
md.appendCodeblock(`define ${sig.name}(${sig.params.join(', ')})`, 'tpl');
if (sig.docString) {
md.appendMarkdown(`---\n${sig.docString}`);
}
return new vscode.Hover(md);
}
// Fallback, wenn man nur über dem Namespace (z.B. "discovery") hovert
return new vscode.Hover(new vscode.MarkdownString(`**${word}**\n\n*(BMC Built-in / TKU Namespace)*`));
}
// 2. Lokale Defines
if (parts.length === 1 || parts.length === 2) {
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const defMatch = line.match(new RegExp(`^\\s*define\\s+${funcName}\\b(?:\\s*\\((.*?)\\))?`));
if (defMatch) {
const params = defMatch[1] ? defMatch[1].trim() : '';
const md = new vscode.MarkdownString();
md.appendCodeblock(`define ${funcName}(${params})`, 'tpl');
const textAfterDefine = text.substring(text.indexOf(line) + line.length);
const docMatch = textAfterDefine.match(/^\s*(?:"""|''')([\s\S]*?)(?:"""|''')/);
if (docMatch) { md.appendMarkdown(`\n\n---\n\n${docMatch[1].trim()}`); }
return new vscode.Hover(md);
}
}
}
// 3. Cross-File Hover für from ... import
const fromImportRegex = new RegExp(`from\\s+([a-zA-Z0-9_.]+)\\s+import\\s+${importBase}\\b`);
const fromMatch = text.match(fromImportRegex);
if (fromMatch) {
const moduleName = fromMatch[1];
if (funcName !== importBase) {
const files = await vscode.workspace.findFiles('**/*.tpl', '**/node_modules/**');
for (const file of files) {
const doc = await vscode.workspace.openTextDocument(file);
const docText = doc.getText();
if (new RegExp(`tpl\\s+1\\.\\d+\\s+module\\s+${moduleName.replace(/\./g, '\\.')}\\s*;`).test(docText)) {
const docLines = docText.split('\n');
for (let i = 0; i < docLines.length; i++) {
const line = docLines[i];
const defMatch = line.match(new RegExp(`^\\s*define\\s+${funcName}\\b(?:\\s*\\((.*?)\\))?`));
if (defMatch) {
const params = defMatch[1] ? defMatch[1].trim() : '';
const md = new vscode.MarkdownString();
md.appendCodeblock(`define ${funcName}(${params})`, 'tpl');
const textAfterDefine = docText.substring(docText.indexOf(line) + line.length);
const docMatch = textAfterDefine.match(/^\s*(?:"""|''')([\s\S]*?)(?:"""|''')/);
if (docMatch) { md.appendMarkdown(`\n\n---\n\n${docMatch[1].trim()}`); }
md.appendMarkdown(`\n\n*(Importiert aus \`${moduleName}\`)*`);
return new vscode.Hover(md);
}
}
}
}
}
return new vscode.Hover(new vscode.MarkdownString(`Importierte Definitions **${importBase}** aus Modul \`${moduleName}\`.`));
}
return null;
}

View File

@ -1,35 +1,57 @@
import * as vscode from 'vscode';
import { CompletionStrategy } from './CompletionStrategy';
import { TplLookup } from '../../core/TplLookup';
export class BuiltInCompletion implements CompletionStrategy {
public async provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): Promise<vscode.CompletionItem[] | undefined> {
const dotMatch = linePrefix.match(/\b([a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*)\.([a-zA-Z0-9_]*)$/);
public provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): vscode.CompletionItem[] | undefined {
if (dotMatch) {
const prefix = dotMatch[1];
const items: vscode.CompletionItem[] = [];
let isHandled = false;
// Prüft, ob wir im Namespace "log." oder "discovery." sind (auch wenn man danach weiter tippt!)
const match = linePrefix.match(/\b(discovery|log|model|text|regex|time|xpath|json|hash|inference)\.([a-zA-Z0-9_]*)$/);
// NEU: Case-Insensitive Lookup (findet Core UND TKU Methoden!)
const methods = TplLookup.getAllMethodsForNamespace(prefix);
if (match) {
const namespace = match[1];
if (methods) {
isHandled = true;
for (const [funcName, funcData] of Object.entries(methods)) {
const item = new vscode.CompletionItem(funcName, vscode.CompletionItemKind.Method);
item.detail = `TPL Built-in: ${prefix}.${funcName}`;
item.documentation = new vscode.MarkdownString(funcData.description);
if (namespace === 'log') {
const levels = ['debug', 'info', 'warn', 'error'];
for (const level of levels) {
const item = new vscode.CompletionItem(level, vscode.CompletionItemKind.Method);
item.insertText = new vscode.SnippetString(`${level}("\${1:message}");`);
item.documentation = new vscode.MarkdownString(`Schreibt eine Log-Meldung auf dem Level **${level}**.`);
if (funcData.snippets && funcData.snippets.length > 0) {
item.insertText = new vscode.SnippetString(`${funcName}(${funcData.snippets[0]});$0`);
} else {
item.insertText = new vscode.SnippetString(`${funcName}();$0`);
}
items.push(item);
}
}
else if (namespace === 'discovery') {
const runCmd = new vscode.CompletionItem('runCommand', vscode.CompletionItemKind.Method);
runCmd.insertText = new vscode.SnippetString('runCommand(${1:host}, "${2:command}");');
runCmd.detail = "discovery.runCommand(host, command)";
items.push(runCmd);
// Dynamische Konstruktoren für 'model', 'model.rel' etc. bleiben hier unverändert...
if (prefix.toLowerCase() === 'model') {
isHandled = true;
const dynamicItem = new vscode.CompletionItem('<NodeKind>', vscode.CompletionItemKind.Constructor);
dynamicItem.detail = "Dynamic Taxonomy Node";
dynamicItem.documentation = new vscode.MarkdownString("The `model` scope is automatically populated with a function named after each node kind defined in the taxonomy.");
dynamicItem.insertText = new vscode.SnippetString("${1:NodeKind}(key := ${2:key_value});$0");
items.push(dynamicItem);
}
return items; // Treffer gefunden, wir geben sie zurück!
else if (prefix.toLowerCase() === 'model.rel' || prefix.toLowerCase() === 'model.uniquerel') {
isHandled = true;
const dynamicItem = new vscode.CompletionItem('<Relationship>', vscode.CompletionItemKind.Constructor);
dynamicItem.detail = prefix.toLowerCase() === 'model.rel' ? "Dynamic Taxonomy Relationship" : "Dynamic Unique Taxonomy Relationship";
dynamicItem.documentation = new vscode.MarkdownString(`The \`${prefix}\` scope is automatically populated with a function named after each relationship defined in the taxonomy.`);
dynamicItem.insertText = new vscode.SnippetString("${1:RelationshipName}(${2:SourceRole} := ${3:source_node}, ${4:DestinationRole} := ${5:dest_node});$0");
items.push(dynamicItem);
}
if (isHandled) {
return items;
}
}
return undefined;
}
}

View File

@ -6,6 +6,8 @@ export class FormatState {
public parenAlignments: number[] = [];
public bracketAlignments: number[] = [];
public inImportBlock: boolean = false;
// Gibt die aktuellen Basis-Leerzeichen zurück (z.B. Level 2 = 8 Leerzeichen)
public get currentIndentString(): string {
return ' '.repeat(this.indentLevel * this.tabSize);

View File

@ -1,30 +1,88 @@
import { FormatState } from './FormatState';
export class IndentationRules {
// Keywords, die eine neue Einrückungsebene öffnen (ohne static!)
private readonly blockOpens = /^(pattern|syncmapping|table|definitions?|enumeration|configuration|define|metadata|overview|constants|triggers|mapping|removal|body|if\s+|for\s+|while\s+)\b/;
// Keywords, die eine Ebene schließen (ohne static!)
private readonly blockCloses = /^(end\s+|else\b|elif\b)/;
public apply(line: string, state: FormatState): string {
// 1. Wenn die Zeile einen Block schließt, VORHER die Einrückung verringern
if (this.blockCloses.test(line) && state.indentLevel > 0) {
state.indentLevel--;
// 1. Leere Zeilen komplett rasieren
if (line.trim().length === 0) return "";
// 2. Strings und Kommentare sicher entfernen (für die Keyword-Erkennung)
let cleanLine = "";
let inString = false;
let stringChar = '';
let inTripleQuote = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
if (!inString && !inTripleQuote && char === '/' && line[i+1] === '/') break;
if (!inString && !inTripleQuote && (char === '"' || char === "'") && line[i+1] === char && line[i+2] === char) {
inTripleQuote = true; stringChar = char; cleanLine += char+char+char; i += 2; continue;
}
if (inTripleQuote) {
if (char === stringChar && line[i+1] === stringChar && line[i+2] === stringChar) {
inTripleQuote = false; cleanLine += char+char+char; i += 2;
}
continue;
}
if (inString) {
if (char === '\\') i++; else if (char === stringChar) inString = false;
continue;
}
if (!inString && !inTripleQuote && (char === '"' || char === "'")) {
inString = true; stringChar = char; cleanLine += char; continue;
}
cleanLine += char;
}
cleanLine = cleanLine.trim();
// Alle vorhandenen falschen Leerzeichen am Anfang (2, 3, 6, etc.) brutal abschneiden
const trimmedLine = line.trimStart();
// 3. DEDENT: Aktuelle Zeile rückt nach links (end, elif, else)
if (/^end\b/.test(cleanLine)) {
state.indentLevel = Math.max(0, state.indentLevel - 1);
} else if (/^(elif|else)\b/.test(cleanLine)) {
state.indentLevel = Math.max(0, state.indentLevel - 1);
}
// Aktuelle Einrückung für diese Zeile abholen
const currentIndent = state.currentIndentString;
// 4. DIE EINZIGE WAHRHEIT: Harte Spaces setzen
let result = trimmedLine;
// 2. NACH der Zeile: Wenn sie einen Block öffnet, Level für die nächste Zeile erhöhen
if (/^(else\b|elif\b)/.test(line)) {
// else / elif springen nach dem Schließen direkt wieder auf
if (state.parenAlignments && state.parenAlignments.length > 0) {
// Visual Alignment (Runde Klammern) - Das fasst diese Regel nicht an,
// das übernimmt die nachfolgende AlignmentRule.
} else if (state.inImportBlock) {
// Im Multi-Line Import rücken wir zur besseren Lesbarkeit eine Ebene tiefer ein
if (!cleanLine.startsWith('from ')) {
result = " ".repeat((state.indentLevel + 2) * state.tabSize) + trimmedLine;
} else {
result = state.currentIndentString + trimmedLine;
}
} else {
// Standard-Einrückung unter Nutzung deines Getters
result = state.currentIndentString + trimmedLine;
}
// 5. INDENT: Nächste Zeile rückt nach rechts
if (/^(pattern|syncmapping|table|definitions?|define|enumeration|configuration|overview|metadata|constants|triggers|body|mapping|removal)\b/.test(cleanLine)) {
state.indentLevel++;
}
else if (this.blockOpens.test(line)) {
if (/\b(then|do)\b$/.test(cleanLine)) {
state.indentLevel++;
}
// elif/else eröffnen sofort wieder einen neuen Block für die Folgezeile
if (/^(elif|else)\b/.test(cleanLine)) {
state.indentLevel++;
}
return currentIndent + line;
// 6. Multi-Line Import Tracking
if (cleanLine.startsWith('from ')) {
state.inImportBlock = true;
}
if (state.inImportBlock && cleanLine.endsWith(';')) {
state.inImportBlock = false;
}
return result;
}
}