Compare commits

...

5 Commits

Author SHA1 Message Date
kuro 939d9073e6 Merge branch 'master' of https://wiccafe.de/kuro/bmc-tpl-code-all-around 2026-10-28 23:04:53 +01:00
kuro 4f5b7fe1f0 test changes and added nightly build for gitea 2026-10-28 23:04:50 +01:00
kuro 21e6833cb1 Readme update 2026-10-28 21:12:22 +00:00
kuro a63c490276 Readme update 2026-10-28 21:10:39 +00:00
kuro 3b597e6df4 Fixes for common mistakes the current add on had 2026-10-28 22:04:56 +01:00
21 changed files with 8106 additions and 7611 deletions

View File

@ -0,0 +1,46 @@
name: Nightly Build
on:
schedule:
# Läuft jeden Tag um 02:00 Uhr nachts
- cron: '0 2 * * *'
# Erlaubt es dir, den Build im Gitea-Webinterface auch manuell per Button zu starten
workflow_dispatch:
jobs:
build-nightly:
runs-on: ubuntu-latest # Pass das an das Label deines Gitea-Runners an
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24' # Passend zu deiner package.json Umgebung
- name: Install System Dependencies for Headless Tests
run: |
sudo apt-get update
sudo apt-get install -y xvfb libx11-xcb1 libxcomposite1 libxcursor1 libxdamage1 libxi6 libxtst6 libnss3 libcups2 libxss1 libxrandr2 libasound2 libatk1.0-0 libatk-bridge2.0-0 libgtk-3-0
- name: Install NPM Packages
run: npm ci
- name: Inject Nightly Version
run: |
NIGHTLY_DATE=$(date +%Y%m%d)
sed -i "s/\"version\": \"\(.*\)\"/\"version\": \"\1-nightly-$NIGHTLY_DATE\"/" package.json
echo "Building Nightly Version for $NIGHTLY_DATE..."
- name: Package & Test (with xvfb)
# xvfb-run fängt die GUI-Aufrufe der VS Code Test-Engine ab
run: xvfb-run -a npm run package
- name: Upload VSIX Artifact
uses: actions/upload-artifact@v4
with:
name: bmc-tpl-code-all-around-nightly
path: '*.vsix'
retention-days: 14

View File

@ -1,6 +1,4 @@
English Description first - after that comes the German Version. English Description first - after that comes the German Version.
Translation:
Englische beschreibung zuerst, danach kommt die Deutsche
--- ---
@ -20,12 +18,13 @@ The linter analyzes the code in real-time, remembers states across line breaks,
* **Smart Assignment Check:** Finds missing semicolons (but tolerates them in `enumeration` blocks or multi-line statements). Warns strictly with an error if `=` is used instead of `:=` for variable assignments (outside of legitimate `if`/`where` comparisons). * **Smart Assignment Check:** Finds missing semicolons (but tolerates them in `enumeration` blocks or multi-line statements). Warns strictly with an error if `=` is used instead of `:=` for variable assignments (outside of legitimate `if`/`where` comparisons).
* **Block & Bracket Tracking:** Detects unclosed blocks (like missing `end body;` or `end pattern;`) and finds unclosed parentheses `(` or brackets `[`. * **Block & Bracket Tracking:** Detects unclosed blocks (like missing `end body;` or `end pattern;`) and finds unclosed parentheses `(` or brackets `[`.
* **Mandatory Field Validation:** Checks if mandatory module-level metadata (`origin`, `author`, `products`, `publishers`, `tree_path`) are present based on your dynamic compliance settings, and if an initial `log.debug` statement exists in the `body`. * **Mandatory Field Validation:** Checks if mandatory module-level metadata (`origin`, `author`, `products`, `publishers`, `tree_path`) are present based on your dynamic compliance settings, and if an initial `log.debug` statement exists in the `body`.
* **Indentation Checking (New):** Instantly highlights messy indentation (any space count that is not a clean multiple of 4) with a warning, reminding you to use the Auto-Formatter. * **Indentation Checking:** Instantly highlights messy indentation (any space count that is not a clean multiple of 4) with a warning, reminding you to use the Auto-Formatter.
* **Unbreakable Lexer (New):** Built-in state-machine lexer correctly parses complex strings, triple-quotes (`"""`), and comments, ensuring accurate parameter counting and avoiding false-positive errors on complex TPL syntax.
* **Version Checking:** Compares the TPL version declared in the module with the target version defined in your settings and warns about outdated scripts. * **Version Checking:** Compares the TPL version declared in the module with the target version defined in your settings and warns about outdated scripts.
### ⚙️ Configurable Plugin Settings & Compliance ### ⚙️ Configurable Plugin Settings & Compliance
Personalize the plugin directly via the VS Code user interface (File > Preferences > Settings > Extensions > BMC TPL Settings): Personalize the plugin directly via the VS Code user interface (File > Preferences > Settings > Extensions > BMC TPL Settings):
* **Metadata Compliance (New):** Define exact compliance rules for your workspace. Choose which metadata fields are mandatory and configure the `tree_path` strictly (e.g., minimum depth and forced static base nodes like `['MyCompany', 'IT']`). * **Metadata Compliance:** Define exact compliance rules for your workspace. Choose which metadata fields are mandatory and configure the `tree_path` strictly (e.g., minimum depth and forced static base nodes like `['MyCompany', 'IT']`).
* **Default Metadata:** Store your own values for `Author`, `Publisher`, `Origin`, etc. These are automatically inserted when generating new modules. * **Default Metadata:** Store your own values for `Author`, `Publisher`, `Origin`, etc. These are automatically inserted when generating new modules.
* **TPL Target Version:** Define the default TPL version (e.g., `23` for 1.23) you are developing for. * **TPL Target Version:** Define the default TPL version (e.g., `23` for 1.23) you are developing for.
* **Live Reload:** Changes to settings take effect immediately, no editor restart required. * **Live Reload:** Changes to settings take effect immediately, no editor restart required.
@ -35,11 +34,15 @@ No more broken indentations! The formatter gets your code in top shape:
* **VS Code Standard:** Strictly enforces 4 spaces per indentation level. * **VS Code Standard:** Strictly enforces 4 spaces per indentation level.
* **Dynamic Block Indentation:** Automatically detects starting and ending blocks (`if`, `for`, `pattern`, `table`, etc.) and adjusts the indentation level. * **Dynamic Block Indentation:** Automatically detects starting and ending blocks (`if`, `for`, `pattern`, `table`, etc.) and adjusts the indentation level.
* **Bracket Memory (Visual Alignment):** The ultimate discipline! The formatter remembers the exact column position of opened arrays `[` or function calls `(` and flawlessly aligns all subsequent lines to this bracket. * **Bracket Memory (Visual Alignment):** The ultimate discipline! The formatter remembers the exact column position of opened arrays `[` or function calls `(` and flawlessly aligns all subsequent lines to this bracket.
* **Uncompromising Cleanup (New):** Automatically strips all non-standard indentation (like 2, 3, or 6 spaces) and replaces it with exact, mathematically calculated 4-space blocks.
### 💡 IntelliSense & Autocomplete (Cross-File) ### 💡 IntelliSense & Autocomplete (Cross-File)
Saves an extreme amount of coding time through massive context awareness: Saves an extreme amount of coding time through massive context awareness:
* **"Go to Definition" (New):** Instantly navigate your codebase! `Ctrl+Click` (or `F12`) on any function call to jump directly to its declaration. Searches locally first and falls back to the RAM-based workspace index for cross-file jumps.
* **Dynamic Taxonomy Support (New):** Full autocomplete support for dynamic BMC scopes. Typing `model.` suggests generic `<NodeKind>` constructors, while `model.rel.` offers dynamic `<Relationship>` constructors with ready-to-use snippets.
* **Full BMC Standard Library:** Seamless integration of all built-in functions (e.g., `discovery`, `number`, `text`, `model`). Supports parameter overloads (e.g., `number.toText` optionally with or without a base) via Tab navigation. * **Full BMC Standard Library:** Seamless integration of all built-in functions (e.g., `discovery`, `number`, `text`, `model`). Supports parameter overloads (e.g., `number.toText` optionally with or without a base) via Tab navigation.
* **Workspace Scanning:** If you import a module (e.g., `from company.helpers import my_helper`), the plugin scans your entire project in the background. If you type `my_helper.`, it automatically suggests all available `define` methods. * **Workspace Scanning:** If you import a module (e.g., `from company.helpers import my_helper`), the plugin scans your entire project in the background. If you type `my_helper.`, it automatically suggests all available `define` methods.
* **Case-Insensitive Resolution (New):** All method lookups (both Core and TKU) are completely case-insensitive, meaning `rdbms_functions.xyz` works just as flawlessly as `RDBMS_Functions.Xyz`.
* **Local Namespaces:** Detects `definitions` blocks in the same file and suggests their methods. * **Local Namespaces:** Detects `definitions` blocks in the same file and suggests their methods.
* **Variable Tracking:** Learns all variables you have defined with `:=` and suggests them to you later. * **Variable Tracking:** Learns all variables you have defined with `:=` and suggests them to you later.
@ -80,7 +83,8 @@ Just type the keyword and press `Enter` to generate complete, perfectly indented
3. The linter starts automatically with every keystroke. 3. The linter starts automatically with every keystroke.
4. **Formatting:** Right-click -> *Format Document* (or `Shift + Alt + F`). 4. **Formatting:** Right-click -> *Format Document* (or `Shift + Alt + F`).
5. **Autocomplete:** Triggers automatically while typing, especially after a dot (`.`) during module calls. 5. **Autocomplete:** Triggers automatically while typing, especially after a dot (`.`) during module calls.
6. **Quick Fixes:** Click the yellow/red lightbulb at the beginning of the line or press `Ctrl + .` (Windows/Linux) / `Cmd + .` (Mac). 6. **Navigation:** Press `Ctrl` + Click (or `F12`) to jump to function definitions.
7. **Quick Fixes:** Click the yellow/red lightbulb at the beginning of the line or press `Ctrl + .` (Windows/Linux) / `Cmd + .` (Mac).
--- ---
@ -115,12 +119,13 @@ Der Linter analysiert den Code in Echtzeit, merkt sich Zustände über Zeilenumb
* **Smarter Zuweisungs-Check:** Findet fehlende Semikolons (toleriert sie aber in `enumeration`-Blöcken oder bei umgebrochenen Code-Zeilen). Warnt streng mit einem Fehler, wenn `=` anstelle von `:=` für Variablenzuweisungen genutzt wird (außerhalb von legitimen `if`/`where` Vergleichen). * **Smarter Zuweisungs-Check:** Findet fehlende Semikolons (toleriert sie aber in `enumeration`-Blöcken oder bei umgebrochenen Code-Zeilen). Warnt streng mit einem Fehler, wenn `=` anstelle von `:=` für Variablenzuweisungen genutzt wird (außerhalb von legitimen `if`/`where` Vergleichen).
* **Block- & Klammer-Tracking:** Erkennt nicht geschlossene Blöcke (wie fehlende `end body;` oder `end pattern;`) und findet ungeschlossene runde `(` oder eckige `[` Klammern. * **Block- & Klammer-Tracking:** Erkennt nicht geschlossene Blöcke (wie fehlende `end body;` oder `end pattern;`) und findet ungeschlossene runde `(` oder eckige `[` Klammern.
* **Pflichtfeld-Validierung:** Prüft basierend auf deinen dynamischen Compliance-Settings, ob auf Modul-Ebene zwingend benötigte Metadaten (`origin`, `author`, `tree_path` etc.) vorhanden sind und ob im `body` ein initiales `log.debug` Statement steht. * **Pflichtfeld-Validierung:** Prüft basierend auf deinen dynamischen Compliance-Settings, ob auf Modul-Ebene zwingend benötigte Metadaten (`origin`, `author`, `tree_path` etc.) vorhanden sind und ob im `body` ein initiales `log.debug` Statement steht.
* **Einrückungs-Prüfung (Neu):** Ein neuer Check im Hintergrund markiert unsaubere Einrückungen (alles, was kein sauberes Vielfaches von 4 Leerzeichen ist) sofort mit einer Warnung, um an den Auto-Formatter zu erinnern. * **Einrückungs-Prüfung:** Ein Check im Hintergrund markiert unsaubere Einrückungen (alles, was kein sauberes Vielfaches von 4 Leerzeichen ist) sofort mit einer Warnung, um an den Auto-Formatter zu erinnern.
* **Unzerstörbarer Lexer (Neu):** Ein eingebauter State-Machine-Lexer verarbeitet komplexe Strings, Triple-Quotes (`"""`) und Kommentare fehlerfrei. Dadurch zählt der Linter Parameter immer korrekt und wirft keine falschen Fehler bei komplexer TPL-Syntax.
* **Versions-Prüfung:** Gleicht die im Modul deklarierte TPL-Version mit der in deinen Settings definierten Version ab und warnt vor veralteten Skripten. * **Versions-Prüfung:** Gleicht die im Modul deklarierte TPL-Version mit der in deinen Settings definierten Version ab und warnt vor veralteten Skripten.
### ⚙️ Konfigurierbare Plugin-Einstellungen & Compliance ### ⚙️ Konfigurierbare Plugin-Einstellungen & Compliance
Personalisiere das Plugin direkt über die VS Code Benutzeroberfläche (File > Preferences > Settings > Extensions > BMC TPL Einstellungen): Personalisiere das Plugin direkt über die VS Code Benutzeroberfläche (File > Preferences > Settings > Extensions > BMC TPL Einstellungen):
* **Metadaten-Compliance (Neu):** Definiere exakte Regeln für deinen Workspace. Wähle, welche Felder Pflicht sind und konfiguriere den `tree_path` strikt (z.B. Mindest-Ebenen und feste Basis-Nodes wie `['MeineFirma', 'IT']`). * **Metadaten-Compliance:** Definiere exakte Regeln für deinen Workspace. Wähle, welche Felder Pflicht sind und konfiguriere den `tree_path` strikt (z.B. Mindest-Ebenen und feste Basis-Nodes wie `['MeineFirma', 'IT']`).
* **Standard Metadaten:** Hinterlege deine eigenen Werte für `Author`, `Publisher`, `Origin` etc. Diese werden beim Generieren neuer Module automatisch eingesetzt. * **Standard Metadaten:** Hinterlege deine eigenen Werte für `Author`, `Publisher`, `Origin` etc. Diese werden beim Generieren neuer Module automatisch eingesetzt.
* **TPL Zielversion:** Definiere die TPL-Version (z.B. `23` für 1.23), für die du standardmäßig entwickelst. * **TPL Zielversion:** Definiere die TPL-Version (z.B. `23` für 1.23), für die du standardmäßig entwickelst.
* **Live-Reload:** Änderungen an den Einstellungen greifen sofort, ganz ohne Neustart des Editors. * **Live-Reload:** Änderungen an den Einstellungen greifen sofort, ganz ohne Neustart des Editors.
@ -130,11 +135,15 @@ Schluss mit kaputten Einrückungen! Der Formatter bringt deinen Code in Bestform
* **VS Code Standard:** Erzwingt strikt 4 Leerzeichen pro Einrückungs-Ebene. * **VS Code Standard:** Erzwingt strikt 4 Leerzeichen pro Einrückungs-Ebene.
* **Dynamische Block-Einrückung:** Erkennt automatisch startende und endende Blöcke (`if`, `for`, `pattern`, `table` etc.) und passt die Einrückungsebene an. * **Dynamische Block-Einrückung:** Erkennt automatisch startende und endende Blöcke (`if`, `for`, `pattern`, `table` etc.) und passt die Einrückungsebene an.
* **Klammer-Gedächtnis (Visual Alignment):** Die Königsdisziplin! Der Formatter merkt sich die spaltengenaue Position von geöffneten Arrays `[` oder Funktionsaufrufen `(` und richtet alle Folgezeilen makellos an dieser Klammer aus. * **Klammer-Gedächtnis (Visual Alignment):** Die Königsdisziplin! Der Formatter merkt sich die spaltengenaue Position von geöffneten Arrays `[` oder Funktionsaufrufen `(` und richtet alle Folgezeilen makellos an dieser Klammer aus.
* **Kompromisslose Bereinigung (Neu):** Unsaubere Einrückungen (z.B. 2, 3 oder 6 Leerzeichen) werden restlos rasiert und durch exakt berechnete 4-Space-Blöcke ersetzt.
### 💡 IntelliSense & Autovervollständigung (Cross-File) ### 💡 IntelliSense & Autovervollständigung (Cross-File)
Spart extrem viel Zeit beim Coden durch massives Kontext-Wissen: Spart extrem viel Zeit beim Coden durch massives Kontext-Wissen:
* **"Go to Definition" (Neu):** Navigiere sofort durch deine Codebase! `Strg+Klick` (oder `F12`) auf einen Funktionsaufruf springt direkt zur Deklaration. Sucht erst lokal und weicht für dateiübergreifende Aufrufe blitzschnell auf den RAM-Index aus.
* **Dynamische Taxonomie (Neu):** Voller Support für dynamische BMC-Scopes. Das Tippen von `model.` schlägt einen generischen `<NodeKind>`-Konstruktor vor, während `model.rel.` dynamische `<Relationship>`-Konstruktoren mit fertigen Snippets anbietet.
* **Volle BMC Standard-Bibliothek:** Nahtlose Integration aller Built-In Funktionen (z.B. `discovery`, `number`, `text`, `model`). Unterstützt Parameter-Überladungen (z.B. `number.toText` wahlweise mit oder ohne Basis) per Tab-Navigation. * **Volle BMC Standard-Bibliothek:** Nahtlose Integration aller Built-In Funktionen (z.B. `discovery`, `number`, `text`, `model`). Unterstützt Parameter-Überladungen (z.B. `number.toText` wahlweise mit oder ohne Basis) per Tab-Navigation.
* **Workspace-Scanning:** Importierst du ein Modul (z.B. `from company.helpers import my_helper`), durchsucht das Plugin im Hintergrund dein gesamtes Projekt. Tippst du `my_helper.`, werden dir automatisch alle verfügbaren `define`-Methoden vorgeschlagen. * **Workspace-Scanning:** Importierst du ein Modul (z.B. `from company.helpers import my_helper`), durchsucht das Plugin im Hintergrund dein gesamtes Projekt. Tippst du `my_helper.`, werden dir automatisch alle verfügbaren `define`-Methoden vorgeschlagen.
* **Case-Insensitive Auswertung (Neu):** Alle Methoden (Core und TKU) werden nun komplett groß-/kleinschreibungsunabhängig aufgelöst (`rdbms_functions.xyz` funktioniert genauso tadellos wie `RDBMS_Functions.Xyz`).
* **Lokale Namespaces:** Erkennt `definitions`-Blöcke in der gleichen Datei und schlägt deren Methoden vor. * **Lokale Namespaces:** Erkennt `definitions`-Blöcke in der gleichen Datei und schlägt deren Methoden vor.
* **Variablen-Tracking:** Lernt alle Variablen, die du mit `:=` definiert hast, und schlägt sie dir später vor. * **Variablen-Tracking:** Lernt alle Variablen, die du mit `:=` definiert hast, und schlägt sie dir später vor.
@ -175,7 +184,8 @@ Einfach das Keyword tippen und `Enter` drücken, um komplette, perfekt eingerüc
3. Der Linter startet automatisch bei jedem Tastendruck. 3. Der Linter startet automatisch bei jedem Tastendruck.
4. **Formatieren:** Rechtsklick -> *Dokument formatieren* (oder `Shift + Alt + F`). 4. **Formatieren:** Rechtsklick -> *Dokument formatieren* (oder `Shift + Alt + F`).
5. **Autovervollständigung:** Triggert automatisch beim Tippen, speziell nach einem Punkt (`.`) bei Modul-Aufrufen. 5. **Autovervollständigung:** Triggert automatisch beim Tippen, speziell nach einem Punkt (`.`) bei Modul-Aufrufen.
6. **Quick Fixes:** Auf die gelbe/rote Glühbirne am Zeilenanfang klicken oder `Ctrl + .` (Windows/Linux) / `Cmd + .` (Mac) drücken. 6. **Navigieren:** Mit `Strg` + Klick (oder `F12`) direkt zu Funktionsdefinitionen springen.
7. **Quick Fixes:** Auf die gelbe/rote Glühbirne am Zeilenanfang klicken oder `Ctrl + .` (Windows/Linux) / `Cmd + .` (Mac) drücken.
--- ---
@ -186,3 +196,4 @@ Dieses Projekt ist unter der **MIT License** lizenziert.
Copyright (c) 2026 Thomas Wehnert Copyright (c) 2026 Thomas Wehnert
Jedem, der eine Kopie dieser Software erhält, wird hiermit kostenlos die Erlaubnis erteilt, ohne Einschränkung mit der Software zu handeln, einschließlich und ohne Einschränkung der Rechte zur Nutzung, zum Kopieren, Ändern, Zusammenführen, Veröffentlichen, Verteilen, Unterlizenzieren und/oder Verkaufen von Kopien der Software, sofern der obige Urheberrechtshinweis und dieser Genehmigungshinweis in allen Kopien oder wesentlichen Teilen der Software enthalten sind. Die Software wird "ohne Mängelgewähr" (as is) zur Verfügung gestellt. Jedem, der eine Kopie dieser Software erhält, wird hiermit kostenlos die Erlaubnis erteilt, ohne Einschränkung mit der Software zu handeln, einschließlich und ohne Einschränkung der Rechte zur Nutzung, zum Kopieren, Ändern, Zusammenführen, Veröffentlichen, Verteilen, Unterlizenzieren und/oder Verkaufen von Kopien der Software, sofern der obige Urheberrechtshinweis und dieser Genehmigungshinweis in allen Kopien oder wesentlichen Teilen der Software enthalten sind. Die Software wird "ohne Mängelgewähr" (as is) zur Verfügung gestellt.
```eof

View File

@ -50,7 +50,7 @@
"tpl.metadata.publisher": { "tpl.metadata.publisher": {
"type": "string", "type": "string",
"default": "IT-Operations", "default": "IT-Operations",
"description": "Standard-Publisher ffor new generated TPL-Modules." "description": "Standard-Publisher for new generated TPL-Modules."
}, },
"tpl.metadata.treePathBase": { "tpl.metadata.treePathBase": {
"type": "string", "type": "string",
@ -90,13 +90,18 @@
"tpl.metadata.treePath.staticNodes": { "tpl.metadata.treePath.staticNodes": {
"type": "array", "type": "array",
"items": { "type": "string" }, "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." "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": { "tpl.version": {
"type": "number", "type": "number",
"default": 23, "default": 23,
"description": "Aktuelle TPL-Nebenversion (z. B. 23 für TPL 1.23). Wird genutzt um veraltete Module zu erkennen und beim Generieren eingefügt." "description": "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": [ "commands": [
@ -128,13 +133,14 @@
"watch": "npm-run-all -p watch:*", "watch": "npm-run-all -p watch:*",
"watch:esbuild": "node esbuild.js --watch", "watch:esbuild": "node esbuild.js --watch",
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json", "watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"package": "npm run check-types && npm run lint && node esbuild.js --production", "package": "npm run check-types && npm run lint && npm run test && node esbuild.js --production",
"compile-tests": "tsc -p . --outDir out", "compile-tests": "tsc -p . --outDir out",
"watch-tests": "tsc -p . -w --outDir out", "watch-tests": "tsc -p . -w --outDir out",
"pretest": "npm run compile-tests && npm run compile && npm run lint", "pretest": "npm run compile-tests && npm run compile && npm run lint",
"check-types": "tsc --noEmit", "check-types": "tsc --noEmit",
"lint": "eslint src", "lint": "eslint src",
"test": "vscode-test" "test": "vscode-test",
"generate-builtins": "python scripts/generate_builtins.py ."
}, },
"devDependencies": { "devDependencies": {
"@types/vscode": "^1.140.0", "@types/vscode": "^1.140.0",

View File

@ -4,6 +4,7 @@ import { SyntaxRule } from './rules/SyntaxRule';
import { BlockRule } from './rules/BlockRule'; import { BlockRule } from './rules/BlockRule';
import { MetadataRule } from './rules/MetadataRule'; import { MetadataRule } from './rules/MetadataRule';
import { IndentationRule } from './rules/IndentationRule'; import { IndentationRule } from './rules/IndentationRule';
import { ParameterRule } from './rules/ParameterRule';
export function validateTplDocument(document: vscode.TextDocument, collection: vscode.DiagnosticCollection) { export function validateTplDocument(document: vscode.TextDocument, collection: vscode.DiagnosticCollection) {
if (document.languageId !== 'tpl') {return;} if (document.languageId !== 'tpl') {return;}
@ -13,10 +14,11 @@ export function validateTplDocument(document: vscode.TextDocument, collection: v
const ctx = new LinterContext(); const ctx = new LinterContext();
const rules = [ const rules = [
new IndentationRule(),
new SyntaxRule(), new SyntaxRule(),
new MetadataRule(), new MetadataRule(),
new BlockRule(), new BlockRule(),
new IndentationRule() new ParameterRule()
]; ];
for (let i = 0; i < lines.length; i++) { 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,24 +1,22 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
export interface TplFunctionSignature { export interface TplFunctionSignature {
name: string; // z.B. "modelling.doWork" name: string;
moduleName: string; // z.B. "Company.software_modelling" moduleName: string;
params: string[]; // z.B. ["host", "ip_address"] params: string[];
docString?: string; // Extrahierter Kommentar docString?: string;
hasReturnValue?: boolean;
lineIdx: number; // NEU: Für 'Go to Definition'
fsPath: string; // NEU: Für 'Go to Definition'
} }
export class WorkspaceIndex { export class WorkspaceIndex {
// Map: Datei-Pfad -> Map von Funktionen (Funktionsname -> Signatur)
private fileIndex = new Map<string, Map<string, TplFunctionSignature>>(); private fileIndex = new Map<string, Map<string, TplFunctionSignature>>();
// Map: Datei-Pfad -> Modulname
private fileModules = new Map<string, string>(); 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>>(); private moduleDefinitions = new Map<string, Set<string>>();
public updateFile(uri: vscode.Uri, text: string) { public updateFile(uri: vscode.Uri, text: string) {
if (!uri.fsPath.endsWith('.tpl')) return; if (!uri.fsPath.endsWith('.tpl')) {return;}
const fsPath = uri.fsPath; const fsPath = uri.fsPath;
const lines = text.split('\n'); const lines = text.split('\n');
@ -27,22 +25,19 @@ export class WorkspaceIndex {
const functions = new Map<string, TplFunctionSignature>(); const functions = new Map<string, TplFunctionSignature>();
const defBlocks = new Set<string>(); const defBlocks = new Set<string>();
// 1. Modulnamen parsen
const modMatch = text.match(/tpl\s+1\.\d+\s+module\s+([a-zA-Z0-9_.]+)\s*;/); const modMatch = text.match(/tpl\s+1\.\d+\s+module\s+([a-zA-Z0-9_.]+)\s*;/);
if (modMatch) { if (modMatch) {
currentModule = modMatch[1]; currentModule = modMatch[1];
} }
this.fileModules.set(fsPath, currentModule); this.fileModules.set(fsPath, currentModule);
// 2. Blöcke und Funktionen parsen
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const line = lines[i]; const line = lines[i];
// Startet ein Definitions-Block?
const defBlockMatch = line.match(/^\s*definitions?\s+([a-zA-Z0-9_]+)/); const defBlockMatch = line.match(/^\s*definitions?\s+([a-zA-Z0-9_]+)/);
if (defBlockMatch) { if (defBlockMatch) {
currentDefBlock = defBlockMatch[1]; currentDefBlock = defBlockMatch[1];
defBlocks.add(currentDefBlock); // Block merken! defBlocks.add(currentDefBlock);
continue; continue;
} }
if (line.match(/^\s*end\s+definitions?/)) { if (line.match(/^\s*end\s+definitions?/)) {
@ -50,38 +45,44 @@ export class WorkspaceIndex {
continue; continue;
} }
// Patterns, Syncmappings und Tabellen für den Import freigeben
const structMatch = line.match(/^\s*(?:pattern|syncmapping|table)\s+([a-zA-Z0-9_]+)/); const structMatch = line.match(/^\s*(?:pattern|syncmapping|table)\s+([a-zA-Z0-9_]+)/);
if (structMatch) { if (structMatch) {
defBlocks.add(structMatch[1]); defBlocks.add(structMatch[1]);
} }
// Sucht nach 'define name(param1, param2)'
const defineMatch = line.match(/^\s*define\s+([a-zA-Z0-9_]+)\b(?:\s*\((.*?)\))?/); 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 paramString = defineMatch[2] || ""; const paramString = defineMatch[2] || "";
const fullName = currentDefBlock ? `${currentDefBlock}.${funcName}` : funcName; 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); const params = paramString.split(',').map(p => p.trim()).filter(p => p.length > 0);
// DocString extrahieren
let docString = undefined; let docString = undefined;
const textAfterDefine = text.substring(text.indexOf(line) + line.length); const textAfterDefine = text.substring(text.indexOf(line) + line.length);
const docMatch = textAfterDefine.match(/^\s*(?:"""|''')([\s\S]*?)(?:"""|''')/); const docMatch = textAfterDefine.match(/^\s*(?:"""|''')([\s\S]*?)(?:"""|''')/);
if (docMatch) { docString = docMatch[1].trim(); } 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, { functions.set(fullName, {
name: fullName, name: fullName,
moduleName: currentModule, moduleName: currentModule,
params: params, params: params,
docString: docString docString: docString,
hasReturnValue: hasReturnValue,
lineIdx: i,
fsPath: fsPath
}); });
} }
} }
// In den RAM schreiben
this.fileIndex.set(fsPath, functions); this.fileIndex.set(fsPath, functions);
if (currentModule) { if (currentModule) {
this.moduleDefinitions.set(currentModule, defBlocks); this.moduleDefinitions.set(currentModule, defBlocks);
@ -97,20 +98,15 @@ export class WorkspaceIndex {
this.fileModules.delete(uri.fsPath); this.fileModules.delete(uri.fsPath);
} }
// --- HILFSFUNKTIONEN FÜR PROVIDER UND LINTER ---
// Liefert alle Module (z.B. für "from [Autocomplete] import ...")
public getAllModules(): string[] { public getAllModules(): string[] {
return Array.from(this.moduleDefinitions.keys()); 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[] { public getDefinitionBlocksForModule(moduleName: string): string[] {
const blocks = this.moduleDefinitions.get(moduleName); const blocks = this.moduleDefinitions.get(moduleName);
return blocks ? Array.from(blocks) : []; return blocks ? Array.from(blocks) : [];
} }
// Liefert alle Funktionen eines Moduls (für "modelling.[Autocomplete]")
public getFunctionsForModule(moduleName: string): TplFunctionSignature[] { public getFunctionsForModule(moduleName: string): TplFunctionSignature[] {
const result: TplFunctionSignature[] = []; const result: TplFunctionSignature[] = [];
for (const [fsPath, map] of this.fileIndex.entries()) { for (const [fsPath, map] of this.fileIndex.entries()) {
@ -120,6 +116,18 @@ export class WorkspaceIndex {
} }
return result; 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(); 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 { public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void {
if (!ctx.hasModuleDef) { if (!ctx.hasModuleDef) {
if (line.startsWith('tpl ')) { ctx.hasModuleDef = true; } if (line.startsWith('tpl ')) { ctx.hasModuleDef = true; }
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')) { if (ctx.inModuleMetadata || (line.startsWith('metadata') && ctx.inBlock === 'none')) {
return; return;
} }
if (line.match(/^(pattern|syncmapping|table|definitions?|enumeration|configuration)\b/)) { if (line.match(/^(pattern|syncmapping|table|definitions?|enumeration|configuration)\b/)) {
if (!ctx.hasModuleMetadata && ctx.inBlock === 'none') { 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 { public checkEnd(lines: string[], ctx: LinterContext): void {
for (const unclosedLine of ctx.openDefines) { 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) { 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,33 +52,33 @@ export class BlockRule implements LinterRule {
private closeControlBlock(expectedType: string, line: string, lineIdx: number, ctx: LinterContext) { private closeControlBlock(expectedType: string, line: string, lineIdx: number, ctx: LinterContext) {
if (ctx.openControlBlocks.length === 0) { 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; return;
} }
const lastBlock = ctx.openControlBlocks.pop()!; const lastBlock = ctx.openControlBlocks.pop()!;
if (lastBlock.type !== expectedType) { if (lastBlock.type !== expectedType) {
ctx.addDiag(lineIdx, 0, line.length, `Wrong end. Expected: 'end ${lastBlock.type};', found: 'end${expectedType};'.`, vscode.DiagnosticSeverity.Error); ctx.addDiag(lineIdx, 0, line.length, `Wrong end. Expected: 'end ${lastBlock.type};', found: 'end ${expectedType};'.`, vscode.DiagnosticSeverity.Error);
ctx.openControlBlocks.push(lastBlock); ctx.openControlBlocks.push(lastBlock);
} }
} }
private handleBlockOpenings(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) { private handleBlockOpenings(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
if (line.startsWith('definitions ') || line.startsWith('definition ')) { 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'; ctx.inBlock = 'definitions';
} }
else if (line.startsWith('define ')) { ctx.openDefines.push(lineIdx); } else if (line.startsWith('define ')) { ctx.openDefines.push(lineIdx); }
else if (line.startsWith('enumeration ')) { 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'; ctx.inBlock = 'enumeration';
} }
else if (line.startsWith('configuration ')) { 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'; ctx.inBlock = 'configuration';
} }
else if (line.startsWith('pattern ') || line.startsWith('syncmapping ')) { 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.inBlock = 'pattern';
ctx.currentPatternPhase = PatternPhase.None; ctx.currentPatternPhase = PatternPhase.None;
ctx.activePhaseName = ""; ctx.activePhaseName = "";
@ -86,10 +86,10 @@ export class BlockRule implements LinterRule {
const nameMatch = line.match(/(pattern|syncmapping)\s+([a-zA-Z0-9_]+)\s+\d+\.\d+/); const nameMatch = line.match(/(pattern|syncmapping)\s+([a-zA-Z0-9_]+)\s+\d+\.\d+/);
if (nameMatch) { ctx.currentPatternName = nameMatch[2]; } 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 ')) { else if (line.startsWith('table ')) {
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, 5, "A table can not be declared inside another block.", vscode.DiagnosticSeverity.Error); } if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, 5, "A table cannot be declared inside another block.", vscode.DiagnosticSeverity.Error); }
ctx.inBlock = 'table'; ctx.inBlock = 'table';
} }
} }
@ -97,8 +97,8 @@ export class BlockRule implements LinterRule {
private checkPatternPhases(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]) { private checkPatternPhases(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]) {
const checkPhase = (phaseName: string, phaseEnum: PatternPhase) => { const checkPhase = (phaseName: string, phaseEnum: PatternPhase) => {
if (line.startsWith(phaseName)) { 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.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, `'${phaseName}' Block ist hier nicht erlaubt.`, 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.currentPatternPhase = phaseEnum;
ctx.activePhaseName = phaseName; ctx.activePhaseName = phaseName;
@ -114,10 +114,14 @@ export class BlockRule implements LinterRule {
checkPhase('removal', PatternPhase.Removal); checkPhase('removal', PatternPhase.Removal);
if (line.startsWith('body')) { 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.activePhaseName = "body";
ctx.activePhaseLine = lineIdx; ctx.activePhaseLine = lineIdx;
const config = vscode.workspace.getConfiguration('tpl');
const requireLog = config.get<boolean>('rules.requireBodyLog', true);
if (requireLog) {
let foundLog = false; let foundLog = false;
for (let j = lineIdx + 1; j < lines.length; j++) { for (let j = lineIdx + 1; j < lines.length; j++) {
const aheadLine = lines[j].trim(); const aheadLine = lines[j].trim();
@ -127,17 +131,18 @@ export class BlockRule implements LinterRule {
if (!foundLog) { if (!foundLog) {
const expectedLog = `log.debug("Start pattern ${ctx.currentPatternName}");`; 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.code = 'missing_log_start';
diag.source = expectedLog; diag.source = expectedLog;
} }
} }
}
const closers = ['end metadata', 'end overview', 'end constants', 'end triggers', 'end mapping', 'end removal', 'end body']; const closers = ['end metadata', 'end overview', 'end constants', 'end triggers', 'end mapping', 'end removal', 'end body'];
for (const closer of closers) { for (const closer of closers) {
if (line.startsWith(closer)) { if (line.startsWith(closer)) {
const phase = closer.replace('end ', '').replace(';', '').trim(); 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 = ""; } else { ctx.activePhaseName = ""; }
} }
} }
@ -145,20 +150,20 @@ export class BlockRule implements LinterRule {
private checkEndBlocks(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) { private checkEndBlocks(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
if (line.startsWith('end pattern') || line.startsWith('end syncmapping')) { 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.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, "'end pattern' found without open pattern block.", 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'; ctx.inBlock = 'none';
} }
else if (line.startsWith('end table') || line.startsWith('end enumeration') || line.startsWith('end configuration')) { else if (line.startsWith('end table') || line.startsWith('end enumeration') || line.startsWith('end configuration')) {
ctx.inBlock = 'none'; ctx.inBlock = 'none';
} }
else if (line.startsWith('end definitions') || line.startsWith('end definition')) { 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'; ctx.inBlock = 'none';
} }
else if (line.startsWith('end define')) { else if (line.startsWith('end define')) {
if (ctx.openDefines.length > 0) { ctx.openDefines.pop(); } 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.trim().length === 0) {return;}
if (rawLine.includes('\t')) {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! // 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; 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 { TplDocumentFormatter } from './providers/TplDocumentFormatter';
import { TplCompletionProvider } from './providers/TplCompletionProvider'; import { TplCompletionProvider } from './providers/TplCompletionProvider';
import { TplHoverProvider } from './providers/TplHoverProvider'; 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) { export function activate(context: vscode.ExtensionContext) {
const diagnosticCollection = vscode.languages.createDiagnosticCollection('tpl-scanner'); 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.registerDocumentFormattingEditProvider('tpl', new TplDocumentFormatter()));
context.subscriptions.push(vscode.languages.registerCompletionItemProvider('tpl', new TplCompletionProvider(), '.')); context.subscriptions.push(vscode.languages.registerCompletionItemProvider('tpl', new TplCompletionProvider(), '.'));
context.subscriptions.push(vscode.languages.registerHoverProvider('tpl', new TplHoverProvider())); context.subscriptions.push(vscode.languages.registerHoverProvider('tpl', new TplHoverProvider()));
context.subscriptions.push(vscode.languages.registerDefinitionProvider('tpl', new TplDefinitionProvider()));
context.subscriptions.push(vscode.commands.registerCommand('tpl.scanWorkspace', () => { context.subscriptions.push(vscode.commands.registerCommand('tpl.scanWorkspace', () => {
scanWorkspace(diagnosticCollection); 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 * as vscode from 'vscode';
import { tplBuiltIns } from '../data/TplBuiltIns'; import { TplLookup } from '../core/TplLookup';
import { generatedTplBuiltIns } from '../data/GeneratedTplBuiltIns'; // Pfad ggf. anpassen import { workspaceIndex } from '../core/WorkspaceIndex';
// Beide Datenquellen zusammenführen
const allTplFunctions: Record<string, any> = { ...tplBuiltIns, ...generatedTplBuiltIns };
export class TplHoverProvider implements vscode.HoverProvider { 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_.]+/); const range = document.getWordRangeAtPosition(position, /[a-zA-Z0-9_.]+/);
if (!range) {return null;} if (!range) {return null;}
const word = document.getText(range); const word = document.getText(range);
const text = document.getText();
const lines = text.split('\n');
const parts = word.split('.'); 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; const funcName = parts.length > 1 ? parts[1] : word;
const signatures = workspaceIndex.getFunctionSignatures(funcName);
// 1. BMC BUILT-INS & TKU DEFINES DYNAMISCH AUSLESEN if (signatures.length > 0) {
const bmcNamespaces = Object.keys(allTplFunctions); const sig = signatures[0];
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 md = new vscode.MarkdownString(); const md = new vscode.MarkdownString();
md.appendCodeblock(`define ${sig.name}(${sig.params.join(', ')})`, 'tpl');
// Wir zeigen den echten Methodennamen als Code-Block if (sig.docString) {
md.appendCodeblock(`${importBase}.${funcName}`, 'tpl'); md.appendMarkdown(`---\n${sig.docString}`);
md.appendMarkdown(`\n\n---\n\n${bmcDoc.description}`); }
return new vscode.Hover(md); 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; return null;
} }

View File

@ -1,35 +1,57 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { CompletionStrategy } from './CompletionStrategy'; import { CompletionStrategy } from './CompletionStrategy';
import { TplLookup } from '../../core/TplLookup';
export class BuiltInCompletion implements CompletionStrategy { 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[] = []; const items: vscode.CompletionItem[] = [];
let isHandled = false;
// Prüft, ob wir im Namespace "log." oder "discovery." sind (auch wenn man danach weiter tippt!) // NEU: Case-Insensitive Lookup (findet Core UND TKU Methoden!)
const match = linePrefix.match(/\b(discovery|log|model|text|regex|time|xpath|json|hash|inference)\.([a-zA-Z0-9_]*)$/); const methods = TplLookup.getAllMethodsForNamespace(prefix);
if (match) { if (methods) {
const namespace = match[1]; 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') { if (funcData.snippets && funcData.snippets.length > 0) {
const levels = ['debug', 'info', 'warn', 'error']; item.insertText = new vscode.SnippetString(`${funcName}(${funcData.snippets[0]});$0`);
for (const level of levels) { } else {
const item = new vscode.CompletionItem(level, vscode.CompletionItemKind.Method); item.insertText = new vscode.SnippetString(`${funcName}();$0`);
item.insertText = new vscode.SnippetString(`${level}("\${1:message}");`); }
item.documentation = new vscode.MarkdownString(`Schreibt eine Log-Meldung auf dem Level **${level}**.`);
items.push(item); items.push(item);
} }
} }
else if (namespace === 'discovery') {
const runCmd = new vscode.CompletionItem('runCommand', vscode.CompletionItemKind.Method); // Dynamische Konstruktoren für 'model', 'model.rel' etc. bleiben hier unverändert...
runCmd.insertText = new vscode.SnippetString('runCommand(${1:host}, "${2:command}");'); if (prefix.toLowerCase() === 'model') {
runCmd.detail = "discovery.runCommand(host, command)"; isHandled = true;
items.push(runCmd); 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; return undefined;
} }
} }

View File

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

View File

@ -1,30 +1,79 @@
import { FormatState } from './FormatState'; import { FormatState } from './FormatState';
export class IndentationRules { 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 { public apply(line: string, state: FormatState): string {
// 1. Wenn die Zeile einen Block schließt, VORHER die Einrückung verringern if (line.trim().length === 0) {return "";}
if (this.blockCloses.test(line) && state.indentLevel > 0) {
state.indentLevel--; 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();
const trimmedLine = line.trimStart();
// 3. DEDENT
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 // 4. SET SPACES
const currentIndent = state.currentIndentString; let result = trimmedLine;
// 2. NACH der Zeile: Wenn sie einen Block öffnet, Level für die nächste Zeile erhöhen if (state.parenAlignments && state.parenAlignments.length > 0) {
if (/^(else\b|elif\b)/.test(line)) { // Visual Alignment greift später
// else / elif springen nach dem Schließen direkt wieder auf } else if (state.inImportBlock) {
if (!cleanLine.startsWith('from ')) {
result = " ".repeat((state.indentLevel + 2) * state.tabSize) + trimmedLine;
} else {
result = state.currentIndentString + trimmedLine;
}
} else {
result = state.currentIndentString + trimmedLine;
}
// 5. INDENT (Gefixt: Mutually Exclusive / else if)
if (/^(pattern|syncmapping|table|definitions?|define|enumeration|configuration|overview|metadata|constants|triggers|body|mapping|removal)\b/.test(cleanLine)) {
state.indentLevel++; state.indentLevel++;
} } else if (/\b(then|do)\b$/.test(cleanLine)) {
else if (this.blockOpens.test(line)) { state.indentLevel++;
} else if (/^else\b/.test(cleanLine)) {
state.indentLevel++; state.indentLevel++;
} }
return currentIndent + line; // 6. Import Block Tracking
if (cleanLine.startsWith('from ')) {
state.inImportBlock = true;
}
if (state.inImportBlock && cleanLine.endsWith(';')) {
state.inImportBlock = false;
}
return result;
} }
} }

9
src/test/fixtures/format_bad.tpl vendored Normal file
View File

@ -0,0 +1,9 @@
tpl 1.21 module TestFormat;
define format_me()
// 6 Leerzeichen hier!
log.debug("2 Leerzeichen hier");
if true then
log.debug("3 Leerzeichen!");
end if;
end define;

14
src/test/fixtures/lexer_edge_cases.tpl vendored Normal file
View File

@ -0,0 +1,14 @@
tpl 1.21 module TestEdgeCases;
define my_edge_func()
"""
Ein böser DocString mit ( [ = und if und end
"""
// Ein Kommentar mit DiscoveryFunctions.escapePath(1,2,3,4,5,6)
clean_path := DiscoveryFunctions.escapePath(host, "böse(Klammern)", true);
if test = "ein_string_mit_komma," then
log.debug("alles gut");
end if;
end define;

12
src/test/fixtures/linter_negative.tpl vendored Normal file
View File

@ -0,0 +1,12 @@
tpl 1.21 module TestNegative;
define my_bad_func()
// Unbekannte Funktion (Info)
rdbms_functions.does_not_exist(host);
// Falsche Parameter-Anzahl (Warning)
DiscoveryFunctions.escapePath(host, "path", true, "zu_viel");
// Zuweisung fehlt für Return-Value (Warning)
DiscoveryFunctions.escapePath(host, "path", true);
end define;

View File

@ -0,0 +1,112 @@
import * as assert from 'assert';
import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
import { ParameterRule } from '../../core/rules/ParameterRule';
import { IndentationRules } from '../../providers/formatters/IndentationRules';
import { FormatState } from '../../providers/formatters/FormatState';
import { LinterContext } from '../../core/LinterContext';
import { TplLookup } from '../../core/TplLookup';
import { workspaceIndex } from '../../core/WorkspaceIndex';
class MockLinterContext extends LinterContext {
public diagnostics: vscode.Diagnostic[] = [];
constructor() {
super();
}
public addDiag(lineIdx: number, start: number, end: number, msg: string, severity: vscode.DiagnosticSeverity): vscode.Diagnostic {
const range = new vscode.Range(lineIdx, start, lineIdx, end);
const diag = new vscode.Diagnostic(range, msg, severity);
this.diagnostics.push(diag);
return diag;
}
}
suite('BMC TPL Core Engine Tests', () => {
const fixturesPath = path.resolve(__dirname, '../../../src/test/fixtures');
suite('TplLookup & Autocomplete', () => {
test('Should resolve Core and TKU built-ins case-insensitively', () => {
const runCmd = TplLookup.getBuiltInFunction('discovery', 'runcommand');
assert.ok(runCmd, 'discovery.runCommand should be found ignoring case');
const tkuCmd = TplLookup.getBuiltInFunction('COMMON_FUNCTIONS', 'identify_host');
assert.ok(tkuCmd, 'Common_Functions.identify_host should be found ignoring case');
});
});
suite('ParameterRule (Lexer & Linter)', () => {
test('Should catch negative cases (unknown func, param count, unused return)', () => {
const rule = new ParameterRule();
const ctx = new MockLinterContext();
const originalGetSignatures = workspaceIndex.getFunctionSignatures.bind(workspaceIndex);
workspaceIndex.getFunctionSignatures = (name: string) => {
if (name === 'my_local_func') {
return [{
name: 'my_local_func',
params: ['a', 'b'],
hasReturnValue: true,
docString: '',
moduleName: 'TestNegative',
lineIdx: 1,
fsPath: 'test.tpl'
}];
}
return originalGetSignatures(name);
};
// GEFIXT: "dummy :=" hinzugefügt, um den doppelten Return-Value-Fehler zu vermeiden
const content = `tpl 1.21 module TestNegative;
define test_func()
unknown_module.does_not_exist(host);
dummy := my_local_func(1, 2, 3);
my_local_func(1, 2);
end define;`;
const lines = content.split('\n');
lines.forEach((line, idx) => rule.checkLine(line, line, idx, ctx, lines));
workspaceIndex.getFunctionSignatures = originalGetSignatures;
assert.strictEqual(ctx.diagnostics.length, 3, 'Should find exactly 3 errors in negative fixture');
assert.ok(ctx.diagnostics.some(d => d.message.includes("Unknown function 'unknown_module.does_not_exist'")));
assert.ok(ctx.diagnostics.some(d => d.message.includes("expects 2 parameter(s), but 3 were provided")));
assert.ok(ctx.diagnostics.some(d => d.message.includes("is not used. Did you forget an assignment (:=)?")));
});
test('Should ignore strings, docstrings and comments (Lexer Edge Cases)', () => {
const rule = new ParameterRule();
const ctx = new MockLinterContext();
const content = fs.readFileSync(path.join(fixturesPath, 'lexer_edge_cases.tpl'), 'utf-8');
const lines = content.split('\n');
lines.forEach((line, idx) => rule.checkLine(line, line, idx, ctx, lines));
assert.strictEqual(ctx.diagnostics.length, 0, 'Should find NO errors in lexer edge cases');
});
});
suite('IndentationRules (Auto-Formatter)', () => {
test('Should forcefully fix bad indentation', () => {
const rule = new IndentationRules();
const state = new FormatState();
const content = fs.readFileSync(path.join(fixturesPath, 'format_bad.tpl'), 'utf-8');
const lines = content.split('\n');
const formattedLines = lines.map(line => rule.apply(line, state));
assert.strictEqual(formattedLines[3], ' // 6 Leerzeichen hier!');
assert.strictEqual(formattedLines[4], ' log.debug("2 Leerzeichen hier");');
assert.strictEqual(formattedLines[5], ' if true then');
assert.strictEqual(formattedLines[6], ' log.debug("3 Leerzeichen!");');
assert.strictEqual(formattedLines[7], ' end if;');
});
});
});