Compare commits
No commits in common. "master" and "beta_6" have entirely different histories.
|
|
@ -1,46 +0,0 @@
|
||||||
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
|
|
||||||
27
README.md
27
README.md
|
|
@ -1,4 +1,6 @@
|
||||||
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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -18,13 +20,12 @@ 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:** 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 (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.
|
||||||
* **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:** 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 (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']`).
|
||||||
* **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.
|
||||||
|
|
@ -34,15 +35,11 @@ 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.
|
||||||
|
|
||||||
|
|
@ -83,8 +80,7 @@ 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. **Navigation:** Press `Ctrl` + Click (or `F12`) to jump to function definitions.
|
6. **Quick Fixes:** Click the yellow/red lightbulb at the beginning of the line or press `Ctrl + .` (Windows/Linux) / `Cmd + .` (Mac).
|
||||||
7. **Quick Fixes:** Click the yellow/red lightbulb at the beginning of the line or press `Ctrl + .` (Windows/Linux) / `Cmd + .` (Mac).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -119,13 +115,12 @@ 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:** 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.
|
* **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.
|
||||||
* **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:** 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 (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']`).
|
||||||
* **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.
|
||||||
|
|
@ -135,15 +130,11 @@ 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.
|
||||||
|
|
||||||
|
|
@ -184,8 +175,7 @@ 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. **Navigieren:** Mit `Strg` + Klick (oder `F12`) direkt zu Funktionsdefinitionen springen.
|
6. **Quick Fixes:** Auf die gelbe/rote Glühbirne am Zeilenanfang klicken oder `Ctrl + .` (Windows/Linux) / `Cmd + .` (Mac) drücken.
|
||||||
7. **Quick Fixes:** Auf die gelbe/rote Glühbirne am Zeilenanfang klicken oder `Ctrl + .` (Windows/Linux) / `Cmd + .` (Mac) drücken.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -196,4 +186,3 @@ 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
|
|
||||||
16
package.json
16
package.json
|
|
@ -50,7 +50,7 @@
|
||||||
"tpl.metadata.publisher": {
|
"tpl.metadata.publisher": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "IT-Operations",
|
"default": "IT-Operations",
|
||||||
"description": "Standard-Publisher for new generated TPL-Modules."
|
"description": "Standard-Publisher ffor new generated TPL-Modules."
|
||||||
},
|
},
|
||||||
"tpl.metadata.treePathBase": {
|
"tpl.metadata.treePathBase": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|
@ -90,18 +90,13 @@
|
||||||
"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": "Current TPL-Subversion (e. g. 23 for TPL 1.23). Used to find old Modules and for auto generating the sceleton."
|
"description": "Aktuelle TPL-Nebenversion (z. B. 23 für TPL 1.23). Wird genutzt um veraltete Module zu erkennen und beim Generieren eingefügt."
|
||||||
},
|
|
||||||
"tpl.rules.requireBodyLog": {
|
|
||||||
"type": "boolean",
|
|
||||||
"default": true,
|
|
||||||
"description": "Require a log.debug statement at the start of a pattern's body phase."
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"commands": [
|
"commands": [
|
||||||
|
|
@ -133,14 +128,13 @@
|
||||||
"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 && npm run test && node esbuild.js --production",
|
"package": "npm run check-types && npm run lint && 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",
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,6 @@ export class LinterContext {
|
||||||
public inMultiLineComment = false;
|
public inMultiLineComment = false;
|
||||||
public inConditionBlock = false;
|
public inConditionBlock = false;
|
||||||
|
|
||||||
// Multi-Line Import-Tracking
|
|
||||||
public inImportBlock = false;
|
|
||||||
public currentImportModule = "";
|
|
||||||
|
|
||||||
// String-Tracking (Basis-Lexer)
|
// String-Tracking (Basis-Lexer)
|
||||||
public inTripleDouble = false;
|
public inTripleDouble = false;
|
||||||
public inTripleSingle = false;
|
public inTripleSingle = false;
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,9 @@ import { LinterContext } from './LinterContext';
|
||||||
import { SyntaxRule } from './rules/SyntaxRule';
|
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'; // 1. Hier importieren
|
||||||
import { ParameterRule } from './rules/ParameterRule';
|
|
||||||
|
// ... (deine eventuellen anderen Hilfs-Imports wie trackStrings) ...
|
||||||
|
|
||||||
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,12 +14,12 @@ export function validateTplDocument(document: vscode.TextDocument, collection: v
|
||||||
const lines = text.split('\n');
|
const lines = text.split('\n');
|
||||||
const ctx = new LinterContext();
|
const ctx = new LinterContext();
|
||||||
|
|
||||||
|
// 2. Hier nur die 3 existierenden Regeln reinlegen (MetadataRule entfernt!)
|
||||||
const rules = [
|
const rules = [
|
||||||
new IndentationRule(),
|
|
||||||
new SyntaxRule(),
|
new SyntaxRule(),
|
||||||
new MetadataRule(),
|
new MetadataRule(),
|
||||||
new BlockRule(),
|
new BlockRule(),
|
||||||
new ParameterRule()
|
new IndentationRule()
|
||||||
];
|
];
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
|
@ -27,12 +28,7 @@ export function validateTplDocument(document: vscode.TextDocument, collection: v
|
||||||
|
|
||||||
ctx.resetLineStates();
|
ctx.resetLineStates();
|
||||||
|
|
||||||
// NEU: TODO-Check hier im Linter, BEVOR Kommentare ignoriert werden!
|
// Basis-Lexer: Strings & Kommentare tracken (läuft vor den Regeln!)
|
||||||
const todoMatch = rawLine.match(/\bTODO\b\s*:?\s*(.*)/i);
|
|
||||||
if (todoMatch) {
|
|
||||||
ctx.addDiag(i, todoMatch.index!, rawLine.length, `TODO: ${todoMatch[1].trim()}`, vscode.DiagnosticSeverity.Information);
|
|
||||||
}
|
|
||||||
|
|
||||||
trackStrings(rawLine, line, ctx);
|
trackStrings(rawLine, line, ctx);
|
||||||
|
|
||||||
if (ctx.isInternalStringLine && (ctx.inTripleDouble || ctx.inTripleSingle)) { continue; }
|
if (ctx.isInternalStringLine && (ctx.inTripleDouble || ctx.inTripleSingle)) { continue; }
|
||||||
|
|
@ -53,11 +49,8 @@ export function validateTplDocument(document: vscode.TextDocument, collection: v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sicherer Aufruf für alle Regeln
|
|
||||||
for (const rule of rules) {
|
for (const rule of rules) {
|
||||||
if (rule.checkEnd) {
|
|
||||||
rule.checkEnd(lines, ctx);
|
rule.checkEnd(lines, ctx);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
collection.set(document.uri, ctx.diagnostics);
|
collection.set(document.uri, ctx.diagnostics);
|
||||||
|
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,133 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
|
|
||||||
export interface TplFunctionSignature {
|
|
||||||
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 {
|
|
||||||
private fileIndex = new Map<string, Map<string, TplFunctionSignature>>();
|
|
||||||
private fileModules = new Map<string, string>();
|
|
||||||
private moduleDefinitions = new Map<string, Set<string>>();
|
|
||||||
|
|
||||||
public updateFile(uri: vscode.Uri, text: string) {
|
|
||||||
if (!uri.fsPath.endsWith('.tpl')) {return;}
|
|
||||||
|
|
||||||
const fsPath = uri.fsPath;
|
|
||||||
const lines = text.split('\n');
|
|
||||||
let currentModule = "";
|
|
||||||
let currentDefBlock = "";
|
|
||||||
const functions = new Map<string, TplFunctionSignature>();
|
|
||||||
const defBlocks = new Set<string>();
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const line = lines[i];
|
|
||||||
|
|
||||||
const defBlockMatch = line.match(/^\s*definitions?\s+([a-zA-Z0-9_]+)/);
|
|
||||||
if (defBlockMatch) {
|
|
||||||
currentDefBlock = defBlockMatch[1];
|
|
||||||
defBlocks.add(currentDefBlock);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (line.match(/^\s*end\s+definitions?/)) {
|
|
||||||
currentDefBlock = "";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const structMatch = line.match(/^\s*(?:pattern|syncmapping|table)\s+([a-zA-Z0-9_]+)/);
|
|
||||||
if (structMatch) {
|
|
||||||
defBlocks.add(structMatch[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
const params = paramString.split(',').map(p => p.trim()).filter(p => p.length > 0);
|
|
||||||
|
|
||||||
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,
|
|
||||||
hasReturnValue: hasReturnValue,
|
|
||||||
lineIdx: i,
|
|
||||||
fsPath: fsPath
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.fileIndex.set(fsPath, functions);
|
|
||||||
if (currentModule) {
|
|
||||||
this.moduleDefinitions.set(currentModule, defBlocks);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeFile(uri: vscode.Uri) {
|
|
||||||
const moduleName = this.fileModules.get(uri.fsPath);
|
|
||||||
if (moduleName) {
|
|
||||||
this.moduleDefinitions.delete(moduleName);
|
|
||||||
}
|
|
||||||
this.fileIndex.delete(uri.fsPath);
|
|
||||||
this.fileModules.delete(uri.fsPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getAllModules(): string[] {
|
|
||||||
return Array.from(this.moduleDefinitions.keys());
|
|
||||||
}
|
|
||||||
|
|
||||||
public getDefinitionBlocksForModule(moduleName: string): string[] {
|
|
||||||
const blocks = this.moduleDefinitions.get(moduleName);
|
|
||||||
return blocks ? Array.from(blocks) : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public getFunctionsForModule(moduleName: string): TplFunctionSignature[] {
|
|
||||||
const result: TplFunctionSignature[] = [];
|
|
||||||
for (const [fsPath, map] of this.fileIndex.entries()) {
|
|
||||||
if (this.fileModules.get(fsPath) === moduleName) {
|
|
||||||
result.push(...map.values());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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();
|
|
||||||
|
|
@ -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 must start with 'tpl 1.x module <name>;'.", vscode.DiagnosticSeverity.Error); ctx.hasModuleDef = true; }
|
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Jede Datei muss mit 'tpl 1.x module <name>;' beginnen.", vscode.DiagnosticSeverity.Error); ctx.hasModuleDef = true; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we are inside the metadata block, we ignore the rest. MetadataRule handles it!
|
// Wenn wir im Metadata-Block sind, ignorieren wir den Rest. Die MetadataRule übernimmt!
|
||||||
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 at the module level is mandatory before defining other blocks.", vscode.DiagnosticSeverity.Error);
|
ctx.addDiag(lineIdx, 0, line.length, "Ein 'metadata' Block auf Modul-Ebene ist Pflicht, bevor andere Blöcke definiert werden.", 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 closed with 'end define;'!", vscode.DiagnosticSeverity.Error);
|
ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Block 'define' wurde nicht mit 'end define;' geschlossen!", 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 closed with 'end ${block.type};'!`, vscode.DiagnosticSeverity.Error);
|
ctx.addDiag(block.lineIdx, 0, lines[block.lineIdx].length, `Block '${block.type}' wurde nicht mit 'end${block.type};' geschlossen!`, 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, `Found 'end ${expectedType}' without an open block.`, vscode.DiagnosticSeverity.Error);
|
ctx.addDiag(lineIdx, 0, line.length, `'end ${expectedType}' gefunden, ohne offenen 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, `Falsches Ende. Erwartet: 'end ${lastBlock.type};', gefunden: '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 'definitions' block must be declared at the top level.", vscode.DiagnosticSeverity.Error); }
|
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "Ein Definitions-Block muss auf oberster Ebene deklariert werden.", 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, "An 'enumeration' must be declared at the top level.", vscode.DiagnosticSeverity.Error); }
|
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "Eine Enumeration muss auf oberster Ebene deklariert werden.", 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' must be declared at the top level.", vscode.DiagnosticSeverity.Error); }
|
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "Eine Konfiguration muss auf oberster Ebene deklariert werden.", 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 or syncmapping cannot be declared inside another block.", vscode.DiagnosticSeverity.Error); }
|
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, 7, "Ein Pattern/Syncmapping kann nicht innerhalb eines anderen Blocks deklariert werden.", 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, "Ungültige Pattern-Deklaration. Erwartet: '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 cannot be declared inside another block.", vscode.DiagnosticSeverity.Error); }
|
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, 5, "Eine Tabelle kann nicht innerhalb eines anderen Blocks deklariert werden.", 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}' was not closed with 'end ${ctx.activePhaseName};'.`, vscode.DiagnosticSeverity.Error); }
|
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, `Block '${phaseName}' is not allowed here.`, vscode.DiagnosticSeverity.Error); }
|
if (ctx.currentPatternPhase >= phaseEnum) { ctx.addDiag(lineIdx, 0, phaseName.length, `'${phaseName}' Block ist hier nicht erlaubt.`, vscode.DiagnosticSeverity.Error); }
|
||||||
|
|
||||||
ctx.currentPatternPhase = phaseEnum;
|
ctx.currentPatternPhase = phaseEnum;
|
||||||
ctx.activePhaseName = phaseName;
|
ctx.activePhaseName = phaseName;
|
||||||
|
|
@ -114,27 +114,22 @@ 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}' was not closed with 'end ${ctx.activePhaseName};'.`, vscode.DiagnosticSeverity.Error); }
|
if (ctx.activePhaseName !== "") { ctx.addDiag(ctx.activePhaseLine, 0, ctx.activePhaseName.length, `Block '${ctx.activePhaseName}' wurde nicht mit 'end${ctx.activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); }
|
||||||
ctx.activePhaseName = "body";
|
ctx.activePhaseName = "body";
|
||||||
ctx.activePhaseLine = lineIdx;
|
ctx.activePhaseLine = lineIdx;
|
||||||
|
|
||||||
const config = vscode.workspace.getConfiguration('tpl');
|
let foundLog = false;
|
||||||
const requireLog = config.get<boolean>('rules.requireBodyLog', true);
|
for (let j = lineIdx + 1; j < lines.length; j++) {
|
||||||
|
const aheadLine = lines[j].trim();
|
||||||
|
if (aheadLine.startsWith('end pattern') || aheadLine.startsWith('end syncmapping')) { break; }
|
||||||
|
if (aheadLine.includes('log.debug') && aheadLine.includes(ctx.currentPatternName)) { foundLog = true; break; }
|
||||||
|
}
|
||||||
|
|
||||||
if (requireLog) {
|
if (!foundLog) {
|
||||||
let foundLog = false;
|
const expectedLog = `log.debug("Start pattern ${ctx.currentPatternName}");`;
|
||||||
for (let j = lineIdx + 1; j < lines.length; j++) {
|
const diag = ctx.addDiag(lineIdx, 0, rawLine.length, `Im Body fehlt das Log-Statement mit dem Pattern-Namen. Bsp: ${expectedLog}`, vscode.DiagnosticSeverity.Error);
|
||||||
const aheadLine = lines[j].trim();
|
diag.code = 'missing_log_start';
|
||||||
if (aheadLine.startsWith('end pattern') || aheadLine.startsWith('end syncmapping')) { break; }
|
diag.source = expectedLog;
|
||||||
if (aheadLine.includes('log.debug') && aheadLine.includes(ctx.currentPatternName)) { foundLog = true; break; }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!foundLog) {
|
|
||||||
const expectedLog = `log.debug("Start pattern ${ctx.currentPatternName}");`;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,7 +137,7 @@ export class BlockRule implements LinterRule {
|
||||||
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, `Unexpected '${closer}'. Expected 'end ${ctx.activePhaseName};'.`, vscode.DiagnosticSeverity.Error); }
|
if (ctx.activePhaseName !== phase) { ctx.addDiag(lineIdx, 0, line.length, `Unerwartetes '${closer}'. Erwartet wurde 'end ${ctx.activePhaseName};'.`, vscode.DiagnosticSeverity.Error); }
|
||||||
else { ctx.activePhaseName = ""; }
|
else { ctx.activePhaseName = ""; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -150,20 +145,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}' was not closed with 'end ${ctx.activePhaseName};'.`, vscode.DiagnosticSeverity.Error); }
|
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, "Found 'end pattern' without an open pattern block.", vscode.DiagnosticSeverity.Error); }
|
if (ctx.inBlock !== 'pattern') { ctx.addDiag(lineIdx, 0, 11, "'end pattern' gefunden, ohne offenen 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, "Found 'end definitions' without an open definitions block.", vscode.DiagnosticSeverity.Error); }
|
if (ctx.inBlock !== 'definitions') { ctx.addDiag(lineIdx, 0, line.length, "'end definitions' gefunden, ohne offenen 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, "Found 'end define' without an open 'define'.", vscode.DiagnosticSeverity.Error); }
|
else { ctx.addDiag(lineIdx, 0, line.length, "'end define' gefunden, ohne offenes 'define'.", vscode.DiagnosticSeverity.Error); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5,30 +5,28 @@ import { LinterContext } from '../LinterContext';
|
||||||
export class IndentationRule implements LinterRule {
|
export class IndentationRule implements LinterRule {
|
||||||
|
|
||||||
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void {
|
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void {
|
||||||
|
// Leere Zeilen oder Zeilen, die nur aus Leerzeichen/Tabs bestehen, ignorieren
|
||||||
if (rawLine.trim().length === 0) {return;}
|
if (rawLine.trim().length === 0) {return;}
|
||||||
if (rawLine.includes('\t')) {return;}
|
|
||||||
|
|
||||||
// Sobald wir in Klammern oder einem Multi-Line Import sind (Visual Alignment),
|
// Wenn Tabs drin sind, meckert die SyntaxRule bereits. Das ignorieren wir hier.
|
||||||
// greift die 4-Leerzeichen-Regel nicht!
|
if (rawLine.includes('\t')) {return;}
|
||||||
if (ctx.parenStack.length > 0 || ctx.bracketStack.length > 0 || ctx.inImportBlock) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const leadingSpacesMatch = rawLine.match(/^ +/);
|
const leadingSpacesMatch = rawLine.match(/^ +/);
|
||||||
const spaceCount = leadingSpacesMatch ? leadingSpacesMatch[0].length : 0;
|
const spaceCount = leadingSpacesMatch ? leadingSpacesMatch[0].length : 0;
|
||||||
|
|
||||||
|
// Ist die Anzahl der Leerzeichen kein Vielfaches von 4?
|
||||||
if (spaceCount > 0 && spaceCount % 4 !== 0) {
|
if (spaceCount > 0 && spaceCount % 4 !== 0) {
|
||||||
ctx.addDiag(
|
ctx.addDiag(
|
||||||
lineIdx,
|
lineIdx,
|
||||||
0,
|
0,
|
||||||
spaceCount,
|
spaceCount, // Markiert genau den Bereich der Leerzeichen
|
||||||
`Invalid indentation (${spaceCount} spaces). Please use a multiple of 4. (Tip: Shift+Alt+F to auto-format)`,
|
`Unsaubere Einrückung (${spaceCount} Leerzeichen). Nutze ein Vielfaches von 4. (Tipp: Shift+Alt+F zum Auto-Formatieren)`,
|
||||||
vscode.DiagnosticSeverity.Warning
|
vscode.DiagnosticSeverity.Warning // Warning reicht hier völlig, Error wäre beim Tippen zu aggressiv
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public checkEnd(lines: string[], ctx: LinterContext): void {
|
public checkEnd(lines: string[], ctx: LinterContext): void {
|
||||||
// empty
|
// Hier muss am Ende der Datei nichts geprüft werden
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -15,11 +15,13 @@ export class MetadataRule implements LinterRule {
|
||||||
if (ctx.inModuleMetadata) {
|
if (ctx.inModuleMetadata) {
|
||||||
const config = vscode.workspace.getConfiguration('tpl');
|
const config = vscode.workspace.getConfiguration('tpl');
|
||||||
|
|
||||||
|
// Felder erfassen
|
||||||
if (line.match(/^origin\b/)) {ctx.foundModuleMetadata.add('origin');}
|
if (line.match(/^origin\b/)) {ctx.foundModuleMetadata.add('origin');}
|
||||||
if (line.match(/^author\b/)) {ctx.foundModuleMetadata.add('author');}
|
if (line.match(/^author\b/)) {ctx.foundModuleMetadata.add('author');}
|
||||||
if (line.match(/^products\b/)) {ctx.foundModuleMetadata.add('products');}
|
if (line.match(/^products\b/)) {ctx.foundModuleMetadata.add('products');}
|
||||||
if (line.match(/^publishers\b/)) {ctx.foundModuleMetadata.add('publishers');}
|
if (line.match(/^publishers\b/)) {ctx.foundModuleMetadata.add('publishers');}
|
||||||
|
|
||||||
|
// Tree-Path Sonderprüfung
|
||||||
if (line.match(/^tree_path\b/)) {
|
if (line.match(/^tree_path\b/)) {
|
||||||
ctx.foundModuleMetadata.add('tree_path');
|
ctx.foundModuleMetadata.add('tree_path');
|
||||||
if (config.get<boolean>('metadata.require.treePath', true)) {
|
if (config.get<boolean>('metadata.require.treePath', true)) {
|
||||||
|
|
@ -27,6 +29,7 @@ export class MetadataRule implements LinterRule {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Block-Ende: Pflichtfelder abgleichen
|
||||||
if (line.startsWith('end metadata')) {
|
if (line.startsWith('end metadata')) {
|
||||||
ctx.inModuleMetadata = false;
|
ctx.inModuleMetadata = false;
|
||||||
this.checkRequiredFields(ctx, config);
|
this.checkRequiredFields(ctx, config);
|
||||||
|
|
@ -36,19 +39,20 @@ export class MetadataRule implements LinterRule {
|
||||||
|
|
||||||
private checkTreePath(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, config: vscode.WorkspaceConfiguration) {
|
private checkTreePath(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, config: vscode.WorkspaceConfiguration) {
|
||||||
const minLevels = config.get<number>('metadata.treePath.minLevels', 3);
|
const minLevels = config.get<number>('metadata.treePath.minLevels', 3);
|
||||||
const staticNodes = config.get<string[]>('metadata.treePath.staticNodes', ['Grafing']); // Fallback
|
const staticNodes = config.get<string[]>('metadata.treePath.staticNodes', ['Grafing']);
|
||||||
|
|
||||||
|
// Extrahiert die Strings aus tree_path := 'A', 'B';
|
||||||
const match = line.match(/tree_path\s*:=\s*(.*);/);
|
const match = line.match(/tree_path\s*:=\s*(.*);/);
|
||||||
if (match) {
|
if (match) {
|
||||||
const paths = match[1].split(',').map(s => s.trim().replace(/^['"]|['"]$/g, ''));
|
const paths = match[1].split(',').map(s => s.trim().replace(/^['"]|['"]$/g, ''));
|
||||||
|
|
||||||
if (paths.length < minLevels) {
|
if (paths.length < minLevels) {
|
||||||
ctx.addDiag(lineIdx, 0, rawLine.length, `The tree_path must contain at least ${minLevels} levels.`, vscode.DiagnosticSeverity.Error);
|
ctx.addDiag(lineIdx, 0, rawLine.length, `Der tree_path muss mindestens ${minLevels} Ebenen haben!`, vscode.DiagnosticSeverity.Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < staticNodes.length; i++) {
|
for (let i = 0; i < staticNodes.length; i++) {
|
||||||
if (paths[i] !== staticNodes[i]) {
|
if (paths[i] !== staticNodes[i]) {
|
||||||
ctx.addDiag(lineIdx, 0, rawLine.length, `Tree path level ${i + 1} must strictly be '${staticNodes[i]}'.`, vscode.DiagnosticSeverity.Error);
|
ctx.addDiag(lineIdx, 0, rawLine.length, `Die tree_path Ebene ${i + 1} muss zwingend '${staticNodes[i]}' lauten!`, vscode.DiagnosticSeverity.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -65,7 +69,7 @@ export class MetadataRule implements LinterRule {
|
||||||
const missing = requiredFields.filter(f => !ctx.foundModuleMetadata.has(f));
|
const missing = requiredFields.filter(f => !ctx.foundModuleMetadata.has(f));
|
||||||
|
|
||||||
if (missing.length > 0 && ctx.moduleMetadataBlockLine !== -1) {
|
if (missing.length > 0 && ctx.moduleMetadataBlockLine !== -1) {
|
||||||
const diag = ctx.addDiag(ctx.moduleMetadataBlockLine, 0, 8, `Missing mandatory module metadata fields: ${missing.join(', ')}`, vscode.DiagnosticSeverity.Error);
|
const diag = ctx.addDiag(ctx.moduleMetadataBlockLine, 0, 8, `Fehlende Pflicht-Metadaten (Modul-Ebene): ${missing.join(', ')}`, vscode.DiagnosticSeverity.Error);
|
||||||
diag.code = 'missing_module_metadata';
|
diag.code = 'missing_module_metadata';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,157 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,56 +1,23 @@
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { LinterRule } from './LinterRule';
|
import { LinterRule } from './LinterRule';
|
||||||
import { LinterContext } from '../LinterContext';
|
import { LinterContext } from '../LinterContext';
|
||||||
import { workspaceIndex } from '../WorkspaceIndex';
|
|
||||||
|
|
||||||
export class SyntaxRule implements LinterRule {
|
export class SyntaxRule 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 {
|
||||||
this.checkTabs(rawLine, lineIdx, ctx);
|
this.checkTabs(rawLine, lineIdx, ctx);
|
||||||
|
|
||||||
|
const todoMatch = rawLine.match(/\bTODO\s*:(.*)/i);
|
||||||
|
if (todoMatch) {
|
||||||
|
ctx.addDiag(lineIdx, todoMatch.index!, rawLine.length, `TODO: ${todoMatch[1].trim()}`, vscode.DiagnosticSeverity.Information);
|
||||||
|
}
|
||||||
|
|
||||||
let cleanLine = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, '""');
|
let cleanLine = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, '""');
|
||||||
const commentIdx = cleanLine.indexOf('//');
|
const commentIdx = cleanLine.indexOf('//');
|
||||||
let codePart = commentIdx !== -1 ? cleanLine.substring(0, commentIdx).trim() : cleanLine.trim();
|
let codePart = commentIdx !== -1 ? cleanLine.substring(0, commentIdx).trim() : cleanLine.trim();
|
||||||
|
|
||||||
if (codePart.length === 0) {return;}
|
if (codePart.length === 0) {return;}
|
||||||
|
|
||||||
// --- MULTI-LINE IMPORT HANDLING & VALIDATION ---
|
|
||||||
if (codePart.startsWith('from ')) {
|
|
||||||
const match = codePart.match(/^from\s+([a-zA-Z0-9_.]+)\s+import/);
|
|
||||||
if (match) {
|
|
||||||
ctx.inImportBlock = true;
|
|
||||||
ctx.currentImportModule = match[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctx.inImportBlock) {
|
|
||||||
// Remove 'from X import' and any trailing semicolons
|
|
||||||
const importString = codePart.replace(/^from\s+[a-zA-Z0-9_.]+\s+import/, '').replace(/;$/, '');
|
|
||||||
const imports = importString.split(',').filter(i => i.trim().length > 0);
|
|
||||||
|
|
||||||
// Check against RAM index
|
|
||||||
const availableItems = workspaceIndex.getDefinitionBlocksForModule(ctx.currentImportModule);
|
|
||||||
|
|
||||||
// Only warn if we actually have the module in RAM, but the specific item is missing
|
|
||||||
if (availableItems.length > 0) {
|
|
||||||
for (const imp of imports) {
|
|
||||||
const cleanImp = imp.trim().split(/\s+/)[0]; // Trims version number (e.g. "HostToPoolMember 2.0" -> "HostToPoolMember")
|
|
||||||
if (cleanImp.length > 0 && !availableItems.includes(cleanImp)) {
|
|
||||||
const idx = rawLine.indexOf(cleanImp);
|
|
||||||
ctx.addDiag(lineIdx, Math.max(0, idx), Math.max(0, idx) + cleanImp.length, `The item '${cleanImp}' does not exist in module '${ctx.currentImportModule}'.`, vscode.DiagnosticSeverity.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the line ends with a semicolon, the import block is closed
|
|
||||||
if (codePart.endsWith(';')) {
|
|
||||||
ctx.inImportBlock = false;
|
|
||||||
ctx.currentImportModule = "";
|
|
||||||
}
|
|
||||||
return; // Skip all other syntax checks for lines inside an import block
|
|
||||||
}
|
|
||||||
// ------------------------------------------------
|
|
||||||
|
|
||||||
let isStructuralKeyword = /^(tpl|pattern|syncmapping|table|definitions?|define|enumeration|configuration|metadata|overview|constants|triggers|mapping|removal|body|end|on|tags|origin|author|products|publishers|tree_path|import|from)\b/.test(codePart);
|
let isStructuralKeyword = /^(tpl|pattern|syncmapping|table|definitions?|define|enumeration|configuration|metadata|overview|constants|triggers|mapping|removal|body|end|on|tags|origin|author|products|publishers|tree_path|import|from)\b/.test(codePart);
|
||||||
|
|
||||||
if (codePart.startsWith('"') || codePart.startsWith("'")) {
|
if (codePart.startsWith('"') || codePart.startsWith("'")) {
|
||||||
|
|
@ -69,9 +36,9 @@ export class SyntaxRule implements LinterRule {
|
||||||
ctx.inBlock === 'definitions' ||
|
ctx.inBlock === 'definitions' ||
|
||||||
ctx.openDefines.length > 0;
|
ctx.openDefines.length > 0;
|
||||||
|
|
||||||
// 'then' and 'do' on standalone lines are legitimate code
|
// 'then' und 'do' auf alleinstehenden Zeilen sind legitimer Code
|
||||||
if (!isStructuralKeyword && !isValidExecutionArea && !/^(then|do)\b/.test(codePart)) {
|
if (!isStructuralKeyword && !isValidExecutionArea && !/^(then|do)\b/.test(codePart)) {
|
||||||
ctx.addDiag(lineIdx, 0, rawLine.length, "Executable code is not allowed here. (Allowed in: 'body', 'triggers', 'constants', 'overview', 'mapping', 'metadata', 'definitions', or 'define').", vscode.DiagnosticSeverity.Error);
|
ctx.addDiag(lineIdx, 0, rawLine.length, "Ausführbarer Code ist hier nicht erlaubt. (Erlaubt in: 'body', 'triggers', 'constants', 'overview', 'mapping', 'metadata', 'definitions' oder 'define').", vscode.DiagnosticSeverity.Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.checkBrackets(codePart, rawLine, lineIdx, ctx);
|
this.checkBrackets(codePart, rawLine, lineIdx, ctx);
|
||||||
|
|
@ -80,14 +47,14 @@ export class SyntaxRule implements LinterRule {
|
||||||
}
|
}
|
||||||
|
|
||||||
public checkEnd(lines: string[], ctx: LinterContext): void {
|
public checkEnd(lines: string[], ctx: LinterContext): void {
|
||||||
for (const unclosedLine of ctx.parenStack) { ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Opening parenthesis '(' is not closed!", vscode.DiagnosticSeverity.Error); }
|
for (const unclosedLine of ctx.parenStack) { ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Öffnende Klammer '(' wird nicht geschlossen!", vscode.DiagnosticSeverity.Error); }
|
||||||
for (const unclosedLine of ctx.bracketStack) { ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Opening bracket '[' is not closed!", vscode.DiagnosticSeverity.Error); }
|
for (const unclosedLine of ctx.bracketStack) { ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Öffnende eckige Klammer '[' wird nicht geschlossen!", vscode.DiagnosticSeverity.Error); }
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkTabs(rawLine: string, lineIdx: number, ctx: LinterContext) {
|
private checkTabs(rawLine: string, lineIdx: number, ctx: LinterContext) {
|
||||||
const tabIndex = rawLine.indexOf('\t');
|
const tabIndex = rawLine.indexOf('\t');
|
||||||
if (tabIndex !== -1) {
|
if (tabIndex !== -1) {
|
||||||
const diag = ctx.addDiag(lineIdx, tabIndex, tabIndex + 1, "Please replace tabs with spaces", vscode.DiagnosticSeverity.Warning);
|
const diag = ctx.addDiag(lineIdx, tabIndex, tabIndex + 1, "Bitte Tabs durch Spaces ersetzen", vscode.DiagnosticSeverity.Warning);
|
||||||
diag.code = 'invalid_tab';
|
diag.code = 'invalid_tab';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -98,12 +65,12 @@ export class SyntaxRule implements LinterRule {
|
||||||
if (char === '(') { ctx.parenStack.push(lineIdx); }
|
if (char === '(') { ctx.parenStack.push(lineIdx); }
|
||||||
else if (char === ')') {
|
else if (char === ')') {
|
||||||
if (ctx.parenStack.length > 0) { ctx.parenStack.pop(); }
|
if (ctx.parenStack.length > 0) { ctx.parenStack.pop(); }
|
||||||
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Unexpected closing parenthesis ')'. Missing an opening parenthesis.", vscode.DiagnosticSeverity.Error); }
|
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Unerwartete schließende Klammer ')'. Es fehlt eine öffnende Klammer.", vscode.DiagnosticSeverity.Error); }
|
||||||
}
|
}
|
||||||
else if (char === '[') { ctx.bracketStack.push(lineIdx); }
|
else if (char === '[') { ctx.bracketStack.push(lineIdx); }
|
||||||
else if (char === ']') {
|
else if (char === ']') {
|
||||||
if (ctx.bracketStack.length > 0) { ctx.bracketStack.pop(); }
|
if (ctx.bracketStack.length > 0) { ctx.bracketStack.pop(); }
|
||||||
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Unexpected closing bracket ']'. Missing an opening bracket.", vscode.DiagnosticSeverity.Error); }
|
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Unerwartete schließende eckige Klammer ']'. Es fehlt eine öffnende Klammer.", vscode.DiagnosticSeverity.Error); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -115,7 +82,7 @@ export class SyntaxRule implements LinterRule {
|
||||||
if (!ctx.inConditionBlock && ctx.parenStack.length === 0 && ctx.bracketStack.length === 0 && !/\b(matches)\b/.test(codePart)) {
|
if (!ctx.inConditionBlock && ctx.parenStack.length === 0 && ctx.bracketStack.length === 0 && !/\b(matches)\b/.test(codePart)) {
|
||||||
const idx = rawLine.indexOf('=');
|
const idx = rawLine.indexOf('=');
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
ctx.addDiag(lineIdx, idx, idx + 1, "Invalid assignment. In TPL, ':=' is used for assignments.", vscode.DiagnosticSeverity.Error);
|
ctx.addDiag(lineIdx, idx, idx + 1, "Falsche Zuweisung. In TPL wird ':=' für Zuweisungen genutzt.", vscode.DiagnosticSeverity.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -126,20 +93,20 @@ export class SyntaxRule implements LinterRule {
|
||||||
private checkSemicolons(codePart: string, rawLine: string, lineIdx: number, ctx: LinterContext, originalTrimmedLine: string) {
|
private checkSemicolons(codePart: string, rawLine: string, lineIdx: number, ctx: LinterContext, originalTrimmedLine: string) {
|
||||||
if (ctx.isClosingString) {
|
if (ctx.isClosingString) {
|
||||||
if (ctx.stringNeedsSemicolon && !originalTrimmedLine.endsWith(';')) {
|
if (ctx.stringNeedsSemicolon && !originalTrimmedLine.endsWith(';')) {
|
||||||
ctx.addDiag(lineIdx, Math.max(0, rawLine.length - 1), rawLine.length, "Missing semicolon at the end of the assignment.", vscode.DiagnosticSeverity.Error);
|
ctx.addDiag(lineIdx, Math.max(0, rawLine.length - 1), rawLine.length, "Fehlendes Semikolon am Ende der Zuweisung.", vscode.DiagnosticSeverity.Error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// "then" and "do" can stand alone without a semicolon requirement
|
// "then" und "do" dürfen am Anfang einer Zeile stehen, ohne ein Semikolon zu erfordern
|
||||||
const noSemicolonNeeded = /^(pattern|syncmapping|traverse|table|enumeration|configuration|definitions?|define|metadata|overview|constants|triggers|mapping|removal|body|end\s+|if\s+|else|elif\s+|for\s+|while\s+|then\b|do\b|\/\/|\/\*|\*)/.test(codePart);
|
const noSemicolonNeeded = /^(pattern|syncmapping|traverse|table|enumeration|configuration|definitions?|define|metadata|overview|constants|triggers|mapping|removal|body|end\s+|if\s+|else|elif\s+|for\s+|while\s+|then\b|do\b|\/\/|\/\*|\*)/.test(codePart);
|
||||||
|
|
||||||
// "in" (for loops) is a valid line break
|
// "in" (für for-Schleifen) als gültigen Zeilenumbruch hinzugefügt
|
||||||
const continuesOnNextLine = /(,|and|or|not|\+|-|\*|\/|=|:=|matches|in)$/i.test(codePart);
|
const continuesOnNextLine = /(,|and|or|not|\+|-|\*|\/|=|:=|matches|in)$/i.test(codePart);
|
||||||
|
|
||||||
const endsWithBlockOpener = /\b(then|do)$/.test(codePart);
|
const endsWithBlockOpener = /\b(then|do)$/.test(codePart);
|
||||||
|
|
||||||
if (!noSemicolonNeeded && !ctx.justOpenedString && ctx.parenStack.length === 0 && ctx.bracketStack.length === 0 && ctx.inBlock !== 'enumeration' && !ctx.inConditionBlock && !continuesOnNextLine && !endsWithBlockOpener) {
|
if (!noSemicolonNeeded && !ctx.justOpenedString && ctx.parenStack.length === 0 && ctx.bracketStack.length === 0 && ctx.inBlock !== 'enumeration' && !ctx.inConditionBlock && !continuesOnNextLine && !endsWithBlockOpener) {
|
||||||
if (codePart.length > 0 && !codePart.endsWith(';') && !codePart.endsWith('{') && !codePart.endsWith(',')) {
|
if (codePart.length > 0 && !codePart.endsWith(';') && !codePart.endsWith('{') && !codePart.endsWith(',')) {
|
||||||
ctx.addDiag(lineIdx, Math.max(0, rawLine.length - 1), rawLine.length, "Missing semicolon at the end of the statement.", vscode.DiagnosticSeverity.Error);
|
ctx.addDiag(lineIdx, Math.max(0, rawLine.length - 1), rawLine.length, "Fehlendes Semikolon am Ende der Anweisung.", vscode.DiagnosticSeverity.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -4,35 +4,16 @@ 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';
|
|
||||||
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');
|
||||||
context.subscriptions.push(diagnosticCollection);
|
context.subscriptions.push(diagnosticCollection);
|
||||||
|
|
||||||
// LIVE INDEXING & LINTING
|
// Linter-Events für geöffnete/geänderte Dateien binden
|
||||||
vscode.workspace.onDidOpenTextDocument(doc => {
|
vscode.workspace.onDidOpenTextDocument(doc => validateTplDocument(doc, diagnosticCollection));
|
||||||
if (doc.languageId === 'tpl') {
|
vscode.workspace.onDidChangeTextDocument(event => validateTplDocument(event.document, diagnosticCollection));
|
||||||
workspaceIndex.updateFile(doc.uri, doc.getText());
|
|
||||||
validateTplDocument(doc, diagnosticCollection);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
vscode.workspace.onDidChangeTextDocument(event => {
|
|
||||||
if (event.document.languageId === 'tpl') {
|
|
||||||
workspaceIndex.updateFile(event.document.uri, event.document.getText());
|
|
||||||
validateTplDocument(event.document, diagnosticCollection);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Aufräumen, wenn Dateien gelöscht werden
|
|
||||||
vscode.workspace.onDidDeleteFiles(event => {
|
|
||||||
for (const uri of event.files) {
|
|
||||||
workspaceIndex.removeFile(uri);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Live-Reload für Einstellungen (Scannt das gesamte Projekt neu)
|
||||||
vscode.workspace.onDidChangeConfiguration(e => {
|
vscode.workspace.onDidChangeConfiguration(e => {
|
||||||
if (e.affectsConfiguration('tpl')) {
|
if (e.affectsConfiguration('tpl')) {
|
||||||
scanWorkspace(diagnosticCollection);
|
scanWorkspace(diagnosticCollection);
|
||||||
|
|
@ -44,28 +25,26 @@ 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()));
|
|
||||||
|
|
||||||
|
// NEU: Befehl registrieren, um manuell alle Dateien zu scannen
|
||||||
context.subscriptions.push(vscode.commands.registerCommand('tpl.scanWorkspace', () => {
|
context.subscriptions.push(vscode.commands.registerCommand('tpl.scanWorkspace', () => {
|
||||||
scanWorkspace(diagnosticCollection);
|
scanWorkspace(diagnosticCollection);
|
||||||
vscode.window.showInformationMessage("TPL: Workspace-Scan gestartet.");
|
vscode.window.showInformationMessage("TPL: Workspace-Scan gestartet.");
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Initialer Scan beim Start
|
// NEU: Bei Aktivierung der Extension direkt einmal alles prüfen!
|
||||||
scanWorkspace(diagnosticCollection);
|
scanWorkspace(diagnosticCollection);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Füttert RAM und Linter
|
// Die neue Engine für das projektweite Lesen
|
||||||
async function scanWorkspace(collection: vscode.DiagnosticCollection) {
|
async function scanWorkspace(collection: vscode.DiagnosticCollection) {
|
||||||
|
// Sucht alle .tpl Dateien im gesamten geöffneten Projektordner
|
||||||
const files = await vscode.workspace.findFiles('**/*.tpl', '**/node_modules/**');
|
const files = await vscode.workspace.findFiles('**/*.tpl', '**/node_modules/**');
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
|
// Öffnet die Datei "unsichtbar" im Hintergrund
|
||||||
const doc = await vscode.workspace.openTextDocument(file);
|
const doc = await vscode.workspace.openTextDocument(file);
|
||||||
|
// Jagt sie durch unseren Linter
|
||||||
// ZUERST in den RAM laden...
|
|
||||||
workspaceIndex.updateFile(doc.uri, doc.getText());
|
|
||||||
|
|
||||||
// ... DANN linten
|
|
||||||
validateTplDocument(doc, collection);
|
validateTplDocument(doc, collection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +1,89 @@
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { TplLookup } from '../core/TplLookup';
|
import { tplBuiltIns } from '../data/TplBuiltIns';
|
||||||
import { workspaceIndex } from '../core/WorkspaceIndex';
|
|
||||||
|
|
||||||
export class TplHoverProvider implements vscode.HoverProvider {
|
export class TplHoverProvider implements vscode.HoverProvider {
|
||||||
public provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): vscode.ProviderResult<vscode.Hover> {
|
public async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Hover | null> {
|
||||||
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 parts = word.split('.');
|
const text = document.getText();
|
||||||
|
const lines = text.split('\n');
|
||||||
|
|
||||||
if (parts.length > 1) {
|
const parts = word.split('.');
|
||||||
// 1. Suche in Core & TKU Built-Ins (Case-Insensitive)
|
const importBase = parts[0];
|
||||||
const funcData = TplLookup.getBuiltInFunction(parts[0], parts[1]);
|
const funcName = parts.length > 1 ? parts[1] : word;
|
||||||
if (funcData) {
|
|
||||||
|
// 1. BMC BUILT-INS DYNAMISCH AUSLESEN
|
||||||
|
const bmcNamespaces = Object.keys(tplBuiltIns);
|
||||||
|
|
||||||
|
if (bmcNamespaces.includes(importBase)) {
|
||||||
|
// Wenn die Methode in der Datei existiert, zeige die echte Doku!
|
||||||
|
if (funcName !== importBase && tplBuiltIns[importBase][funcName]) {
|
||||||
|
const bmcDoc = tplBuiltIns[importBase][funcName];
|
||||||
const md = new vscode.MarkdownString();
|
const md = new vscode.MarkdownString();
|
||||||
md.appendCodeblock(`${parts[0]}.${parts[1]}(${funcData.snippets?.[0] || ''})`, 'tpl');
|
|
||||||
md.appendMarkdown(`---\n${funcData.description}`);
|
// Wir zeigen den echten Methodennamen als Code-Block
|
||||||
|
md.appendCodeblock(`${importBase}.${funcName}`, 'tpl');
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Suche in lokalem Workspace (RAM Index)
|
// 3. Cross-File Hover für from ... import
|
||||||
const funcName = parts.length > 1 ? parts[1] : word;
|
const fromImportRegex = new RegExp(`from\\s+([a-zA-Z0-9_.]+)\\s+import\\s+${importBase}\\b`);
|
||||||
const signatures = workspaceIndex.getFunctionSignatures(funcName);
|
const fromMatch = text.match(fromImportRegex);
|
||||||
if (signatures.length > 0) {
|
if (fromMatch) {
|
||||||
const sig = signatures[0];
|
const moduleName = fromMatch[1];
|
||||||
const md = new vscode.MarkdownString();
|
|
||||||
md.appendCodeblock(`define ${sig.name}(${sig.params.join(', ')})`, 'tpl');
|
if (funcName !== importBase) {
|
||||||
if (sig.docString) {
|
const files = await vscode.workspace.findFiles('**/*.tpl', '**/node_modules/**');
|
||||||
md.appendMarkdown(`---\n${sig.docString}`);
|
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(md);
|
return new vscode.Hover(new vscode.MarkdownString(`Importierte Definitions **${importBase}** aus Modul \`${moduleName}\`.`));
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,23 @@
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
|
import { TplQuickFix } from './TplQuickFix';
|
||||||
|
|
||||||
export class LogFix {
|
export class LogFix implements TplQuickFix {
|
||||||
public diagnosticCode = 'missing_log_start';
|
public readonly diagnosticCode = 'missing_log_start';
|
||||||
|
|
||||||
public provideFix(document: vscode.TextDocument, diagnostic: vscode.Diagnostic): vscode.CodeAction[] {
|
public provideFix(document: vscode.TextDocument, diagnostic: vscode.Diagnostic): vscode.CodeAction[] {
|
||||||
const action = new vscode.CodeAction("Insert missing log statement", vscode.CodeActionKind.QuickFix);
|
const fix = new vscode.CodeAction("Fehlendes Log-Statement einfügen", vscode.CodeActionKind.QuickFix);
|
||||||
action.edit = new vscode.WorkspaceEdit();
|
fix.edit = new vscode.WorkspaceEdit();
|
||||||
|
|
||||||
const expectedLog = diagnostic.source;
|
// Der Linter hat uns den exakten String in die 'source' gepackt
|
||||||
|
const expectedLog = diagnostic.source || 'log.debug("Start pattern");';
|
||||||
|
|
||||||
if (expectedLog) {
|
// Füge es eine Zeile unter dem 'body' Keyword ein, sauber eingerückt
|
||||||
const insertPos = new vscode.Position(diagnostic.range.start.line + 1, 0);
|
const insertText = ` ${expectedLog}\n`;
|
||||||
// 8 Leerzeichen Einrückung für den Body-Inhalt
|
fix.edit.insert(document.uri, new vscode.Position(diagnostic.range.start.line + 1, 0), insertText);
|
||||||
action.edit.insert(document.uri, insertPos, ` ${expectedLog}\n`);
|
|
||||||
action.diagnostics = [diagnostic];
|
fix.diagnostics = [diagnostic];
|
||||||
action.isPreferred = true;
|
fix.isPreferred = true;
|
||||||
return [action];
|
|
||||||
}
|
return [fix];
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,19 +1,20 @@
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
|
import { TplQuickFix } from './TplQuickFix';
|
||||||
|
|
||||||
export class TabFix {
|
export class TabFix implements TplQuickFix {
|
||||||
public diagnosticCode = 'invalid_tab';
|
public readonly diagnosticCode = 'invalid_tab';
|
||||||
|
|
||||||
public provideFix(document: vscode.TextDocument, diagnostic: vscode.Diagnostic): vscode.CodeAction[] {
|
public provideFix(document: vscode.TextDocument, diagnostic: vscode.Diagnostic): vscode.CodeAction[] {
|
||||||
const action = new vscode.CodeAction("Convert tabs to 4 spaces", vscode.CodeActionKind.QuickFix);
|
const fix = new vscode.CodeAction("Tabs in 4 Spaces umwandeln", vscode.CodeActionKind.QuickFix);
|
||||||
action.edit = new vscode.WorkspaceEdit();
|
fix.edit = new vscode.WorkspaceEdit();
|
||||||
|
|
||||||
const line = document.lineAt(diagnostic.range.start.line);
|
const line = document.lineAt(diagnostic.range.start.line);
|
||||||
const newText = line.text.replace(/\t/g, ' ');
|
const newText = line.text.replace(/\t/g, ' ');
|
||||||
|
|
||||||
action.edit.replace(document.uri, line.range, newText);
|
fix.edit.replace(document.uri, line.range, newText);
|
||||||
action.diagnostics = [diagnostic];
|
fix.diagnostics = [diagnostic];
|
||||||
action.isPreferred = true;
|
fix.isPreferred = true; // Setzt dies als Standard-Vorschlag
|
||||||
|
|
||||||
return [action];
|
return [fix];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,57 +1,35 @@
|
||||||
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_]*)$/);
|
|
||||||
|
|
||||||
if (dotMatch) {
|
public provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): vscode.CompletionItem[] | undefined {
|
||||||
const prefix = dotMatch[1];
|
const items: vscode.CompletionItem[] = [];
|
||||||
const items: vscode.CompletionItem[] = [];
|
|
||||||
let isHandled = false;
|
|
||||||
|
|
||||||
// NEU: Case-Insensitive Lookup (findet Core UND TKU Methoden!)
|
// Prüft, ob wir im Namespace "log." oder "discovery." sind (auch wenn man danach weiter tippt!)
|
||||||
const methods = TplLookup.getAllMethodsForNamespace(prefix);
|
const match = linePrefix.match(/\b(discovery|log|model|text|regex|time|xpath|json|hash|inference)\.([a-zA-Z0-9_]*)$/);
|
||||||
|
|
||||||
if (methods) {
|
if (match) {
|
||||||
isHandled = true;
|
const namespace = match[1];
|
||||||
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 (funcData.snippets && funcData.snippets.length > 0) {
|
if (namespace === 'log') {
|
||||||
item.insertText = new vscode.SnippetString(`${funcName}(${funcData.snippets[0]});$0`);
|
const levels = ['debug', 'info', 'warn', 'error'];
|
||||||
} else {
|
for (const level of levels) {
|
||||||
item.insertText = new vscode.SnippetString(`${funcName}();$0`);
|
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}**.`);
|
||||||
items.push(item);
|
items.push(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (namespace === 'discovery') {
|
||||||
// Dynamische Konstruktoren für 'model', 'model.rel' etc. bleiben hier unverändert...
|
const runCmd = new vscode.CompletionItem('runCommand', vscode.CompletionItemKind.Method);
|
||||||
if (prefix.toLowerCase() === 'model') {
|
runCmd.insertText = new vscode.SnippetString('runCommand(${1:host}, "${2:command}");');
|
||||||
isHandled = true;
|
runCmd.detail = "discovery.runCommand(host, command)";
|
||||||
const dynamicItem = new vscode.CompletionItem('<NodeKind>', vscode.CompletionItemKind.Constructor);
|
items.push(runCmd);
|
||||||
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);
|
|
||||||
}
|
|
||||||
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 items; // Treffer gefunden, wir geben sie zurück!
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,107 +1,32 @@
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { CompletionStrategy } from './CompletionStrategy';
|
import { CompletionStrategy } from './CompletionStrategy';
|
||||||
import { tplBuiltIns } from '../../data/TplBuiltIns';
|
import { tplBuiltIns } from '../../data/TplBuiltIns';
|
||||||
import { workspaceIndex } from '../../core/WorkspaceIndex';
|
|
||||||
|
|
||||||
export class LocalContextCompletion implements CompletionStrategy {
|
export class LocalContextCompletion implements CompletionStrategy {
|
||||||
public async provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): Promise<vscode.CompletionItem[] | undefined> {
|
public async provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): Promise<vscode.CompletionItem[] | undefined> {
|
||||||
const text = document.getText();
|
const text = document.getText();
|
||||||
const items: vscode.CompletionItem[] = [];
|
const items: vscode.CompletionItem[] = [];
|
||||||
|
|
||||||
const textBeforeCursor = document.getText(new vscode.Range(new vscode.Position(0, 0), position));
|
|
||||||
const lastFromIndex = textBeforeCursor.lastIndexOf('from ');
|
|
||||||
const lastSemicolonIndex = textBeforeCursor.lastIndexOf(';');
|
|
||||||
|
|
||||||
// 1 & 2. MULTI-LINE SMART IMPORT
|
|
||||||
// Wenn das letzte 'from' NACH dem letzten Semikolon kam, sind wir mitten in einem Import-Block!
|
|
||||||
if (lastFromIndex > lastSemicolonIndex || (lastFromIndex !== -1 && lastSemicolonIndex === -1)) {
|
|
||||||
const importContext = textBeforeCursor.substring(lastFromIndex);
|
|
||||||
const match = importContext.match(/^from\s+([a-zA-Z0-9_.]+)\s+import/);
|
|
||||||
|
|
||||||
if (!match) {
|
|
||||||
// Wir tippen noch den Modul-Namen
|
|
||||||
for (const mod of workspaceIndex.getAllModules()) {
|
|
||||||
const item = new vscode.CompletionItem(mod, vscode.CompletionItemKind.Module);
|
|
||||||
item.detail = "Workspace Module";
|
|
||||||
items.push(item);
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
} else {
|
|
||||||
// Wir tippen die zu importierenden Klassen/Patterns (auch nach Zeilenumbrüchen!)
|
|
||||||
const modName = match[1];
|
|
||||||
for (const defBlock of workspaceIndex.getDefinitionBlocksForModule(modName)) {
|
|
||||||
const item = new vscode.CompletionItem(defBlock, vscode.CompletionItemKind.Class);
|
|
||||||
item.detail = `Available in ${modName}`;
|
|
||||||
items.push(item);
|
|
||||||
}
|
|
||||||
return items; // Nichts anderes vorschlagen!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. NAMESPACE CALLS: "prefix."
|
|
||||||
const dotMatch = linePrefix.match(/\b([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]*)$/);
|
const dotMatch = linePrefix.match(/\b([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]*)$/);
|
||||||
|
|
||||||
if (dotMatch) {
|
if (dotMatch) {
|
||||||
const prefix = dotMatch[1];
|
const prefix = dotMatch[1];
|
||||||
|
|
||||||
// BMC Built-ins ignorieren (übernimmt BuiltInCompletion)
|
|
||||||
const bmcNamespaces = Object.keys(tplBuiltIns);
|
const bmcNamespaces = Object.keys(tplBuiltIns);
|
||||||
if (bmcNamespaces.includes(prefix)) {
|
if (bmcNamespaces.includes(prefix)) {
|
||||||
return undefined; // Hier darf undefined sein, weil BuiltInCompletion das fängt
|
return undefined; // Überlässt das Feld komplett der BuiltInCompletion!
|
||||||
}
|
}
|
||||||
|
|
||||||
let funcs = [];
|
const importRegex = new RegExp(`from\\s+([a-zA-Z0-9_.]+)\\s+import\\s+${prefix}\\b`);
|
||||||
|
|
||||||
// A) RAM-Abfrage für importierte Module
|
|
||||||
const importRegex = new RegExp(`from\\s+([a-zA-Z0-9_.]+)\\s+import\\s+[\\s\\S]*?\\b${prefix}\\b`);
|
|
||||||
const match = importRegex.exec(text);
|
const match = importRegex.exec(text);
|
||||||
if (match) {
|
if (match) {
|
||||||
const moduleName = match[1];
|
const moduleName = match[1];
|
||||||
// Filtere Funktionen aus dem importierten Modul, die zu diesem Prefix gehören
|
const externalItems = await this.resolveImportedMethods(moduleName, prefix);
|
||||||
funcs = workspaceIndex.getFunctionsForModule(moduleName).filter(f => f.name.startsWith(`${prefix}.`) || !f.name.includes('.'));
|
return externalItems;
|
||||||
} else {
|
|
||||||
// B) Lokale Definitions in der aktuellen Datei prüfen!
|
|
||||||
const localDefRegex = new RegExp(`^\\s*definitions?\\s+${prefix}\\b`, 'm');
|
|
||||||
if (localDefRegex.test(text)) {
|
|
||||||
const lines = text.split('\n');
|
|
||||||
let inTargetBlock = false;
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.match(new RegExp(`^\\s*definitions?\\s+${prefix}\\b`))) { inTargetBlock = true; continue; }
|
|
||||||
if (line.match(/^\s*end\s+definitions?/)) { inTargetBlock = false; continue; }
|
|
||||||
if (inTargetBlock) {
|
|
||||||
const defineMatch = line.match(/^\s*define\s+([a-zA-Z0-9_]+)\b(?:\s*\((.*?)\))?/);
|
|
||||||
if (defineMatch) {
|
|
||||||
const params = (defineMatch[2] || "").split(',').map(p => p.trim()).filter(p => p.length > 0);
|
|
||||||
funcs.push({ name: defineMatch[1], moduleName: 'local', params: params });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return undefined;
|
||||||
if (funcs.length > 0) {
|
|
||||||
for (const f of funcs) {
|
|
||||||
const shortName = f.name.split('.').pop() || f.name;
|
|
||||||
const item = new vscode.CompletionItem(shortName, vscode.CompletionItemKind.Function);
|
|
||||||
item.detail = f.moduleName === 'local' ? `Local Method` : `Method from ${f.moduleName}`;
|
|
||||||
|
|
||||||
// Dynamische Parameter-Generierung! Baut z.B. ${1:host}, ${2:ip}
|
|
||||||
let paramSnippet = "";
|
|
||||||
if (f.params && f.params.length > 0) {
|
|
||||||
paramSnippet = f.params.map((p, idx) => `\${${idx + 1}:${p}}`).join(', ');
|
|
||||||
}
|
|
||||||
|
|
||||||
item.insertText = new vscode.SnippetString(`${shortName}(${paramSnippet});$0`);
|
|
||||||
items.push(item);
|
|
||||||
}
|
|
||||||
return items; // Gibt exakt die gefundenen Funktionen zurück
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wenn es ein unbekannter Namespace mit Punkt ist, geben wir ein leeres Array [] zurück!
|
|
||||||
// Das zwingt VS Code dazu, KEINE generischen Textwörter vorzuschlagen.
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. LOKALE DEFINITIONS PARSEN (Für den globalen Scope, noch ohne Punkt aufgerufen)
|
|
||||||
const lines = text.split('\n');
|
const lines = text.split('\n');
|
||||||
let currentDefBlock = "";
|
let currentDefBlock = "";
|
||||||
|
|
||||||
|
|
@ -111,42 +36,25 @@ export class LocalContextCompletion implements CompletionStrategy {
|
||||||
if (defBlockMatch) { currentDefBlock = defBlockMatch[1]; continue; }
|
if (defBlockMatch) { currentDefBlock = defBlockMatch[1]; continue; }
|
||||||
if (line.match(/^\s*end\s+definitions?/)) { currentDefBlock = ""; continue; }
|
if (line.match(/^\s*end\s+definitions?/)) { currentDefBlock = ""; continue; }
|
||||||
|
|
||||||
const defineMatch = line.match(/^\s*define\s+([a-zA-Z0-9_]+)\b(?:\s*\((.*?)\))?/);
|
const defineMatch = line.match(/^\s*define\s+([a-zA-Z0-9_]+)\b/);
|
||||||
if (defineMatch) {
|
if (defineMatch) {
|
||||||
const funcName = defineMatch[1];
|
const funcName = defineMatch[1];
|
||||||
const fullName = currentDefBlock ? `${currentDefBlock}.${funcName}` : funcName;
|
const fullName = currentDefBlock ? `${currentDefBlock}.${funcName}` : funcName;
|
||||||
const item = new vscode.CompletionItem(fullName, vscode.CompletionItemKind.Function);
|
const item = new vscode.CompletionItem(fullName, vscode.CompletionItemKind.Function);
|
||||||
item.detail = currentDefBlock ? `Local Function in '${currentDefBlock}'` : `Local Function`;
|
item.detail = currentDefBlock ? `Local Function in '${currentDefBlock}'` : `Local Function`;
|
||||||
|
item.insertText = new vscode.SnippetString(`${fullName}(\${1:params});$0`);
|
||||||
// Dynamische Parameter-Generierung
|
|
||||||
const params = (defineMatch[2] || "").split(',').map(p => p.trim()).filter(p => p.length > 0);
|
|
||||||
let paramSnippet = "";
|
|
||||||
if (params.length > 0) {
|
|
||||||
paramSnippet = params.map((p, idx) => `\${${idx + 1}:${p}}`).join(', ');
|
|
||||||
}
|
|
||||||
|
|
||||||
item.insertText = new vscode.SnippetString(`${fullName}(${paramSnippet});$0`);
|
|
||||||
items.push(item);
|
items.push(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import-Referenzen für den globalen Scope bereitstellen (angepasst für Multi-Line)
|
const fromImportRegex = /from\s+([a-zA-Z0-9_.]+)\s+import\s+([a-zA-Z0-9_]+)/g;
|
||||||
const fromImportRegex = /from\s+([a-zA-Z0-9_.]+)\s+import\s+([\s\S]*?);/g;
|
|
||||||
let matchVar;
|
let matchVar;
|
||||||
while ((matchVar = fromImportRegex.exec(text)) !== null) {
|
while ((matchVar = fromImportRegex.exec(text)) !== null) {
|
||||||
const moduleName = matchVar[1];
|
const item = new vscode.CompletionItem(matchVar[2], vscode.CompletionItemKind.Module);
|
||||||
const imports = matchVar[2].split(',').map(i => i.trim().split(/\s+/)[0]);
|
item.detail = `Imported Definitions from ${matchVar[1]}`;
|
||||||
|
items.push(item);
|
||||||
for (const imp of imports) {
|
|
||||||
if (imp.length > 0) {
|
|
||||||
const item = new vscode.CompletionItem(imp, vscode.CompletionItemKind.Module);
|
|
||||||
item.detail = `Imported from ${moduleName}`;
|
|
||||||
items.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lokale Variablen (:=) vorschlagen
|
|
||||||
const varRegex = /([a-zA-Z0-9_]+)\s*:=/g;
|
const varRegex = /([a-zA-Z0-9_]+)\s*:=/g;
|
||||||
const foundVars = new Set<string>();
|
const foundVars = new Set<string>();
|
||||||
while ((matchVar = varRegex.exec(text)) !== null) {
|
while ((matchVar = varRegex.exec(text)) !== null) {
|
||||||
|
|
@ -158,4 +66,37 @@ export class LocalContextCompletion implements CompletionStrategy {
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async resolveImportedMethods(moduleName: string, defBlockName: string): Promise<vscode.CompletionItem[]> {
|
||||||
|
const items: vscode.CompletionItem[] = [];
|
||||||
|
const files = await vscode.workspace.findFiles('**/*.tpl', '**/node_modules/**');
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const doc = await vscode.workspace.openTextDocument(file);
|
||||||
|
const text = doc.getText();
|
||||||
|
|
||||||
|
const moduleRegex = new RegExp(`tpl\\s+1\\.\\d+\\s+module\\s+${moduleName.replace(/\./g, '\\.')}\\s*;`);
|
||||||
|
if (moduleRegex.test(text)) {
|
||||||
|
const lines = text.split('\n');
|
||||||
|
let inTargetBlock = false;
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.match(new RegExp(`^\\s*definitions?\\s+${defBlockName}\\b`))) { inTargetBlock = true; continue; }
|
||||||
|
if (inTargetBlock && line.match(/^\s*end\s+definitions?/)) { inTargetBlock = false; break; }
|
||||||
|
|
||||||
|
if (inTargetBlock) {
|
||||||
|
const defineMatch = line.match(/^\s*define\s+([a-zA-Z0-9_]+)\b/);
|
||||||
|
if (defineMatch) {
|
||||||
|
const funcName = defineMatch[1];
|
||||||
|
const item = new vscode.CompletionItem(funcName, vscode.CompletionItemKind.Function);
|
||||||
|
item.detail = `Method from ${moduleName}`;
|
||||||
|
item.insertText = new vscode.SnippetString(`${funcName}(\${1:params});$0`);
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6,8 +6,6 @@ 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);
|
||||||
|
|
|
||||||
|
|
@ -1,79 +1,30 @@
|
||||||
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 {
|
||||||
if (line.trim().length === 0) {return "";}
|
// 1. Wenn die Zeile einen Block schließt, VORHER die Einrückung verringern
|
||||||
|
if (this.blockCloses.test(line) && state.indentLevel > 0) {
|
||||||
let cleanLine = "";
|
state.indentLevel--;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. SET SPACES
|
// Aktuelle Einrückung für diese Zeile abholen
|
||||||
let result = trimmedLine;
|
const currentIndent = state.currentIndentString;
|
||||||
|
|
||||||
if (state.parenAlignments && state.parenAlignments.length > 0) {
|
// 2. NACH der Zeile: Wenn sie einen Block öffnet, Level für die nächste Zeile erhöhen
|
||||||
// Visual Alignment greift später
|
if (/^(else\b|elif\b)/.test(line)) {
|
||||||
} else if (state.inImportBlock) {
|
// else / elif springen nach dem Schließen direkt wieder auf
|
||||||
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)) {
|
}
|
||||||
state.indentLevel++;
|
else if (this.blockOpens.test(line)) {
|
||||||
} else if (/^else\b/.test(cleanLine)) {
|
|
||||||
state.indentLevel++;
|
state.indentLevel++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Import Block Tracking
|
return currentIndent + line;
|
||||||
if (cleanLine.startsWith('from ')) {
|
|
||||||
state.inImportBlock = true;
|
|
||||||
}
|
|
||||||
if (state.inImportBlock && cleanLine.endsWith(';')) {
|
|
||||||
state.inImportBlock = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
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;
|
|
||||||
Binary file not shown.
|
|
@ -4,11 +4,11 @@ import { TabFix } from '../../providers/actions/TabFix';
|
||||||
import { LogFix } from '../../providers/actions/LogFix';
|
import { LogFix } from '../../providers/actions/LogFix';
|
||||||
|
|
||||||
suite('Code Actions Test Suite', () => {
|
suite('Code Actions Test Suite', () => {
|
||||||
test('TabFix: Generates Replace-Edit for tabs', () => {
|
test('TabFix: Erzeugt Replace-Edit für Tabulatoren', () => {
|
||||||
const fix = new TabFix();
|
const fix = new TabFix();
|
||||||
const diag = new vscode.Diagnostic(new vscode.Range(0, 0, 0, 1), "Tab found", vscode.DiagnosticSeverity.Warning);
|
const diag = new vscode.Diagnostic(new vscode.Range(0, 0, 0, 1), "Tab found", vscode.DiagnosticSeverity.Warning);
|
||||||
|
|
||||||
// Mock for TextDocument
|
// Mock für TextDocument
|
||||||
const mockDoc = {
|
const mockDoc = {
|
||||||
uri: vscode.Uri.file('test.tpl'),
|
uri: vscode.Uri.file('test.tpl'),
|
||||||
lineAt: () => ({ text: '\t\tcode', range: new vscode.Range(0, 0, 0, 6) })
|
lineAt: () => ({ text: '\t\tcode', range: new vscode.Range(0, 0, 0, 6) })
|
||||||
|
|
@ -16,13 +16,11 @@ suite('Code Actions Test Suite', () => {
|
||||||
|
|
||||||
const actions = fix.provideFix(mockDoc, diag);
|
const actions = fix.provideFix(mockDoc, diag);
|
||||||
assert.strictEqual(actions.length, 1);
|
assert.strictEqual(actions.length, 1);
|
||||||
|
assert.strictEqual(actions[0].title, "Tabs in 4 Spaces umwandeln");
|
||||||
// Update this string if your actual TabFix title differs!
|
|
||||||
assert.strictEqual(actions[0].title, "Convert tabs to 4 spaces");
|
|
||||||
assert.ok(actions[0].edit !== undefined);
|
assert.ok(actions[0].edit !== undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('LogFix: Generates Insert-Edit with log statement', () => {
|
test('LogFix: Erzeugt Insert-Edit mit Log-Statement', () => {
|
||||||
const fix = new LogFix();
|
const fix = new LogFix();
|
||||||
const diag = new vscode.Diagnostic(new vscode.Range(10, 0, 10, 4), "Missing log");
|
const diag = new vscode.Diagnostic(new vscode.Range(10, 0, 10, 4), "Missing log");
|
||||||
diag.source = 'log.debug("Start pattern Test");'; // Linter-Source Simulation
|
diag.source = 'log.debug("Start pattern Test");'; // Linter-Source Simulation
|
||||||
|
|
@ -31,8 +29,6 @@ suite('Code Actions Test Suite', () => {
|
||||||
|
|
||||||
const actions = fix.provideFix(mockDoc, diag);
|
const actions = fix.provideFix(mockDoc, diag);
|
||||||
assert.strictEqual(actions.length, 1);
|
assert.strictEqual(actions.length, 1);
|
||||||
|
assert.strictEqual(actions[0].title, "Fehlendes Log-Statement einfügen");
|
||||||
// Update this string if your actual LogFix title differs!
|
|
||||||
assert.strictEqual(actions[0].title, "Insert missing log statement");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
|
import { BuiltInCompletion } from '../../providers/completions/BuiltInCompletion';
|
||||||
import { SnippetCompletion } from '../../providers/completions/SnippetCompletion';
|
import { SnippetCompletion } from '../../providers/completions/SnippetCompletion';
|
||||||
import { LocalContextCompletion } from '../../providers/completions/LocalContextCompletion';
|
import { LocalContextCompletion } from '../../providers/completions/LocalContextCompletion';
|
||||||
import { workspaceIndex } from '../../core/WorkspaceIndex';
|
|
||||||
|
|
||||||
|
// Hilfsfunktion simuliert ein vscode.TextDocument
|
||||||
function createMockDoc(text: string): any {
|
function createMockDoc(text: string): any {
|
||||||
const lines = text.split('\n');
|
const lines = text.split('\n');
|
||||||
return {
|
return {
|
||||||
|
|
@ -13,68 +14,51 @@ function createMockDoc(text: string): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
suite('Completion Provider Test Suite', () => {
|
suite('Completion Provider Test Suite', () => {
|
||||||
|
test('SnippetCompletion: Kontext-Check (Kein if außerhalb des Bodys)', async () => {
|
||||||
|
const strategy = new SnippetCompletion();
|
||||||
|
|
||||||
// Feeds the RAM index with a dummy file before tests
|
// Simuliert, dass wir im "metadata" Block tippen
|
||||||
suiteSetup(() => {
|
const docText = `pattern Test 1.0\nmetadata\n `;
|
||||||
const dummyUri = vscode.Uri.file('/fake/path/Company.tools.tpl');
|
|
||||||
const dummyText = `
|
|
||||||
tpl 1.23 module Company.tools;
|
|
||||||
definitions network_helpers
|
|
||||||
define ping(host, ip)
|
|
||||||
end define;
|
|
||||||
end definitions;
|
|
||||||
`.trim();
|
|
||||||
workspaceIndex.updateFile(dummyUri, dummyText);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('LocalContextCompletion: "from " suggests only RAM modules', async () => {
|
|
||||||
const strategy = new LocalContextCompletion();
|
|
||||||
const mockDoc = createMockDoc(`from `);
|
|
||||||
const position = new vscode.Position(0, 5);
|
|
||||||
|
|
||||||
const result = await strategy.provideCompletions(mockDoc, position, 'from ');
|
|
||||||
|
|
||||||
assert.ok(result !== undefined);
|
|
||||||
const hasModule = result!.some(item => item.label === 'Company.tools');
|
|
||||||
assert.strictEqual(hasModule, true, "Must suggest the module from RAM.");
|
|
||||||
});
|
|
||||||
|
|
||||||
test('LocalContextCompletion: "from X import " suggests definition blocks', async () => {
|
|
||||||
const strategy = new LocalContextCompletion();
|
|
||||||
const mockDoc = createMockDoc(`from Company.tools import `);
|
|
||||||
const position = new vscode.Position(0, 26);
|
|
||||||
|
|
||||||
const result = await strategy.provideCompletions(mockDoc, position, 'from Company.tools import ');
|
|
||||||
|
|
||||||
assert.ok(result !== undefined);
|
|
||||||
const hasBlock = result!.some(item => item.label === 'network_helpers');
|
|
||||||
assert.strictEqual(hasBlock, true, "Must suggest the definition block from the module.");
|
|
||||||
});
|
|
||||||
|
|
||||||
test('LocalContextCompletion: Blocks fallback for unknown namespace', async () => {
|
|
||||||
const strategy = new LocalContextCompletion();
|
|
||||||
const mockDoc = createMockDoc(`randomstuff.`);
|
|
||||||
const position = new vscode.Position(0, 12);
|
|
||||||
|
|
||||||
const result = await strategy.provideCompletions(mockDoc, position, 'randomstuff.');
|
|
||||||
|
|
||||||
assert.ok(result !== undefined);
|
|
||||||
assert.strictEqual(result!.length, 0, "Must return an empty array to block VS Code text spam.");
|
|
||||||
});
|
|
||||||
test('LocalContextCompletion: Imported namespace generates parameter snippet', async () => {
|
|
||||||
const strategy = new LocalContextCompletion();
|
|
||||||
|
|
||||||
const docText = `from Company.tools import network_helpers;\nnetwork_helpers.`;
|
|
||||||
const mockDoc = createMockDoc(docText);
|
const mockDoc = createMockDoc(docText);
|
||||||
const position = new vscode.Position(1, 16);
|
const position = new vscode.Position(2, 4); // Zeile 2, Cursor am Ende
|
||||||
|
|
||||||
const result = await strategy.provideCompletions(mockDoc, position, 'network_helpers.');
|
const result = await strategy.provideCompletions(mockDoc, position, ' ');
|
||||||
|
|
||||||
assert.ok(result !== undefined);
|
assert.ok(result !== undefined);
|
||||||
const funcItem = result!.find(item => item.label === 'ping');
|
const hasIf = result!.some(item => item.label === 'if');
|
||||||
assert.ok(funcItem !== undefined, "Must find the ping function");
|
assert.strictEqual(hasIf, false, "Darf kein 'if' im metadata Block vorschlagen");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SnippetCompletion: Vorschläge im Body korrekt', async () => {
|
||||||
|
const strategy = new SnippetCompletion();
|
||||||
|
|
||||||
|
// Simuliert, dass wir im "body" tippen
|
||||||
|
const docText = `pattern Test 1.0\nbody\n `;
|
||||||
|
const mockDoc = createMockDoc(docText);
|
||||||
|
const position = new vscode.Position(2, 4);
|
||||||
|
|
||||||
|
const result = await strategy.provideCompletions(mockDoc, position, ' ');
|
||||||
|
|
||||||
|
assert.ok(result !== undefined);
|
||||||
|
const hasIf = result!.some(item => item.label === 'if');
|
||||||
|
assert.strictEqual(hasIf, true, "Muss 'if' im Body vorschlagen");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('LocalContextCompletion: Erkennt lokale defines in Namespaces', async () => {
|
||||||
|
const strategy = new LocalContextCompletion();
|
||||||
|
|
||||||
|
const docText = `definitions my_tools 1.0\n define doWork(host)\n end define;\nend definitions;`;
|
||||||
|
const mockDoc = createMockDoc(docText);
|
||||||
|
const position = new vscode.Position(4, 0);
|
||||||
|
|
||||||
|
const result = await strategy.provideCompletions(mockDoc, position, 'do');
|
||||||
|
|
||||||
|
assert.ok(result !== undefined);
|
||||||
|
const funcItem = result!.find(item => item.label === 'my_tools.doWork');
|
||||||
|
assert.ok(funcItem !== undefined, "Muss die Methode inkl. Namespace extrahieren");
|
||||||
|
|
||||||
|
// Prüft, ob das generierte Snippet das Auto-Semikolon und den $0 Cursor hat
|
||||||
const snippet = funcItem?.insertText as vscode.SnippetString;
|
const snippet = funcItem?.insertText as vscode.SnippetString;
|
||||||
assert.strictEqual(snippet.value, 'ping(${1:host}, ${2:ip});$0', "Must map parameters exactly");
|
assert.ok(snippet.value.includes('my_tools.doWork(${1:params});$0'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
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;');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -4,49 +4,49 @@ import { IndentationRules } from '../../providers/formatters/IndentationRules';
|
||||||
import { AlignmentRules } from '../../providers/formatters/AlignmentRules';
|
import { AlignmentRules } from '../../providers/formatters/AlignmentRules';
|
||||||
|
|
||||||
suite('Formatter Test Suite', () => {
|
suite('Formatter Test Suite', () => {
|
||||||
test('IndentationRules: Indents if/then/end block correctly', () => {
|
test('IndentationRules: if/then/end Block korrekt einrücken', () => {
|
||||||
const indentRules = new IndentationRules();
|
const indentRules = new IndentationRules();
|
||||||
const state = new FormatState();
|
const state = new FormatState();
|
||||||
state.tabSize = 4;
|
state.tabSize = 4;
|
||||||
|
|
||||||
// Line 1: if condition then (expected: 0 spaces, but level jumps to 1 for the next line)
|
// Zeile 1: if condition then (erwartet: 0 Leerzeichen, aber Level springt auf 1 für die nächste Zeile)
|
||||||
let result = indentRules.apply('if x = 1 then', state);
|
let result = indentRules.apply('if x = 1 then', state);
|
||||||
assert.strictEqual(result, 'if x = 1 then');
|
assert.strictEqual(result, 'if x = 1 then');
|
||||||
assert.strictEqual(state.indentLevel, 1);
|
assert.strictEqual(state.indentLevel, 1);
|
||||||
|
|
||||||
// Line 2: normal code in block (expected: 4 spaces)
|
// Zeile 2: normaler Code im Block (erwartet: 4 Leerzeichen)
|
||||||
result = indentRules.apply('log.debug("test");', state);
|
result = indentRules.apply('log.debug("test");', state);
|
||||||
assert.strictEqual(result, ' log.debug("test");');
|
assert.strictEqual(result, ' log.debug("test");');
|
||||||
assert.strictEqual(state.indentLevel, 1);
|
assert.strictEqual(state.indentLevel, 1);
|
||||||
|
|
||||||
// Line 3: end if; (expected: 0 spaces, level jumps to 0 right before output)
|
// Zeile 3: end if; (erwartet: 0 Leerzeichen, Level springt sofort vor der Ausgabe auf 0)
|
||||||
result = indentRules.apply('end if;', state);
|
result = indentRules.apply('end if;', state);
|
||||||
assert.strictEqual(result, 'end if;');
|
assert.strictEqual(result, 'end if;');
|
||||||
assert.strictEqual(state.indentLevel, 0);
|
assert.strictEqual(state.indentLevel, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('IndentationRules: elif and else behave correctly', () => {
|
test('IndentationRules: elif und else verhalten sich korrekt', () => {
|
||||||
const indentRules = new IndentationRules();
|
const indentRules = new IndentationRules();
|
||||||
const state = new FormatState();
|
const state = new FormatState();
|
||||||
state.tabSize = 4;
|
state.tabSize = 4;
|
||||||
state.indentLevel = 1; // We start practically IN an open if-block
|
state.indentLevel = 1; // Wir starten quasi IN einem offenen if-Block
|
||||||
|
|
||||||
// elif must jump back (to 0) and then back to 1 for the following block
|
// elif muss zurückspringen (auf 0) und danach wieder auf 1 für den folgenden Block
|
||||||
let result = indentRules.apply('elif x = 2 then', state);
|
let result = indentRules.apply('elif x = 2 then', state);
|
||||||
assert.strictEqual(result, 'elif x = 2 then');
|
assert.strictEqual(result, 'elif x = 2 then');
|
||||||
assert.strictEqual(state.indentLevel, 1);
|
assert.strictEqual(state.indentLevel, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('AlignmentRules: Visual alignment at parentheses', () => {
|
test('AlignmentRules: Visual Alignment an runden Klammern', () => {
|
||||||
const alignRules = new AlignmentRules();
|
const alignRules = new AlignmentRules();
|
||||||
const state = new FormatState();
|
const state = new FormatState();
|
||||||
|
|
||||||
// 1. Line with opening parenthesis
|
// 1. Zeile mit öffnender Klammer
|
||||||
alignRules.apply('discovery.runCommand(', 'discovery.runCommand(', state);
|
alignRules.apply('discovery.runCommand(', 'discovery.runCommand(', state);
|
||||||
assert.strictEqual(state.parenAlignments.length, 1);
|
assert.strictEqual(state.parenAlignments.length, 1);
|
||||||
assert.strictEqual(state.parenAlignments[0], 21); // 21 characters until after the open parenthesis
|
assert.strictEqual(state.parenAlignments[0], 21); // 21 Zeichen bis nach der offenen Klammer
|
||||||
|
|
||||||
// 2. Line (parameter should be indented exactly to column 21)
|
// 2. Zeile (Parameter soll exakt bis Spalte 21 eingerückt werden)
|
||||||
const result = alignRules.apply('host,', 'host,', state);
|
const result = alignRules.apply('host,', 'host,', state);
|
||||||
assert.strictEqual(result, ' host,');
|
assert.strictEqual(result, ' host,');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,82 +3,73 @@ import * as vscode from 'vscode';
|
||||||
import { LinterContext } from '../../core/LinterContext';
|
import { LinterContext } from '../../core/LinterContext';
|
||||||
import { SyntaxRule } from '../../core/rules/SyntaxRule';
|
import { SyntaxRule } from '../../core/rules/SyntaxRule';
|
||||||
import { BlockRule } from '../../core/rules/BlockRule';
|
import { BlockRule } from '../../core/rules/BlockRule';
|
||||||
import { validateTplDocument } from '../../core/TplLinter';
|
|
||||||
|
|
||||||
suite('Linter Test Suite', () => {
|
suite('Linter Test Suite', () => {
|
||||||
test('SyntaxRule: Ignores URLs and special characters in strings', () => {
|
test('SyntaxRule: Ignoriert URLs und Sonderzeichen in Strings', () => {
|
||||||
const rule = new SyntaxRule();
|
const rule = new SyntaxRule();
|
||||||
const ctx = new LinterContext();
|
const ctx = new LinterContext();
|
||||||
ctx.activePhaseName = 'body'; // Allowed zone
|
ctx.activePhaseName = 'body'; // Erlaubte Zone
|
||||||
|
|
||||||
const rawLine = ' log.debug("https://example.com/api?q=1//test");';
|
const rawLine = ' log.debug("https://example.com/api?q=1//test");';
|
||||||
rule.checkLine(rawLine.trim(), rawLine, 0, ctx, [rawLine]);
|
rule.checkLine(rawLine.trim(), rawLine, 0, ctx, [rawLine]);
|
||||||
|
|
||||||
// Must not throw an error (neither missing semicolon nor broken comment)
|
// Darf keinen Fehler werfen (weder fehlendes Semikolon noch kaputter Kommentar)
|
||||||
assert.strictEqual(ctx.diagnostics.length, 0);
|
assert.strictEqual(ctx.diagnostics.length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Linter: Captures TODOs as Information', () => {
|
test('SyntaxRule: Erfasst TODOs als Information', () => {
|
||||||
const mockDoc = {
|
const rule = new SyntaxRule();
|
||||||
languageId: 'tpl',
|
const ctx = new LinterContext();
|
||||||
getText: () => "tpl 1.0 module Test;\n// TODO: refactor this",
|
|
||||||
uri: vscode.Uri.file('test.tpl')
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
const collection = vscode.languages.createDiagnosticCollection('test-collection');
|
const rawLine = ' // TODO: Diese Funktion refactoren';
|
||||||
validateTplDocument(mockDoc, collection);
|
rule.checkLine(rawLine.trim(), rawLine, 0, ctx, [rawLine]);
|
||||||
|
|
||||||
const diags = collection.get(mockDoc.uri);
|
assert.strictEqual(ctx.diagnostics.length, 1);
|
||||||
assert.strictEqual(diags?.length, 1, "Must find exactly one TODO");
|
assert.strictEqual(ctx.diagnostics[0].severity, vscode.DiagnosticSeverity.Information);
|
||||||
assert.ok(diags![0].message.includes('TODO: refactor this'));
|
assert.ok(ctx.diagnostics[0].message.includes("TODO: Diese Funktion refactoren"));
|
||||||
assert.strictEqual(diags![0].severity, vscode.DiagnosticSeverity.Information);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('SyntaxRule: Multi-line if-condition without semicolon error', () => {
|
test('SyntaxRule: Multi-Line if-Bedingung ohne Semikolon-Fehler', () => {
|
||||||
const rule = new SyntaxRule();
|
const rule = new SyntaxRule();
|
||||||
const ctx = new LinterContext();
|
const ctx = new LinterContext();
|
||||||
ctx.activePhaseName = 'body';
|
ctx.activePhaseName = 'body';
|
||||||
|
|
||||||
// Line 1: or-break
|
// Zeile 1: or-Umbruch
|
||||||
rule.checkLine('if x = 1 or', 'if x = 1 or', 0, ctx, []);
|
rule.checkLine('if x = 1 or', 'if x = 1 or', 0, ctx, []);
|
||||||
assert.strictEqual(ctx.diagnostics.length, 0);
|
assert.strictEqual(ctx.diagnostics.length, 0);
|
||||||
assert.strictEqual(ctx.inConditionBlock, true);
|
assert.strictEqual(ctx.inConditionBlock, true);
|
||||||
|
|
||||||
// Line 2: then-closure
|
// Zeile 2: then-Abschluss
|
||||||
rule.checkLine('y = 2 then', 'y = 2 then', 1, ctx, []);
|
rule.checkLine('y = 2 then', 'y = 2 then', 1, ctx, []);
|
||||||
assert.strictEqual(ctx.diagnostics.length, 0, "'then' must not require a semicolon at the end!");
|
assert.strictEqual(ctx.diagnostics.length, 0, "'then' darf kein Semikolon am Ende verlangen!");
|
||||||
assert.strictEqual(ctx.inConditionBlock, false);
|
assert.strictEqual(ctx.inConditionBlock, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('SyntaxRule: Allowed allocations in Zones (e.g. metadata)', () => {
|
test('SyntaxRule: Akzeptiert Zuweisungen in erlaubten Zonen (z.B. metadata)', () => {
|
||||||
const rule = new SyntaxRule();
|
const rule = new SyntaxRule();
|
||||||
const ctx = new LinterContext();
|
const ctx = new LinterContext();
|
||||||
ctx.inModuleMetadata = true; // We are in module metadata
|
ctx.inModuleMetadata = true; // Wir sind im Modul-Metadata
|
||||||
|
|
||||||
const rawLine = " origin := 'Custom';";
|
const rawLine = " origin := 'Custom';";
|
||||||
rule.checkLine(rawLine.trim(), rawLine, 0, ctx, [rawLine]);
|
rule.checkLine(rawLine.trim(), rawLine, 0, ctx, [rawLine]);
|
||||||
|
|
||||||
assert.strictEqual(ctx.diagnostics.length, 0, "Allocations have to be allowed in metadata");
|
assert.strictEqual(ctx.diagnostics.length, 0, "Zuweisungen müssen in metadata erlaubt sein");
|
||||||
});
|
});
|
||||||
|
|
||||||
test('BlockRule: if/for/while stack tracking', () => {
|
test('BlockRule: if/for/while Stack-Tracking', () => {
|
||||||
const mockDoc = {
|
const rule = new BlockRule();
|
||||||
languageId: 'tpl',
|
const ctx = new LinterContext();
|
||||||
getText: () => "tpl 1.0 module Test;\nif condition then\nend for;",
|
ctx.hasModuleDef = true;
|
||||||
uri: vscode.Uri.file('test.tpl')
|
ctx.inBlock = 'pattern';
|
||||||
} as any;
|
|
||||||
|
|
||||||
const collection = vscode.languages.createDiagnosticCollection('test-collection-blockrule');
|
// Öffnet Block
|
||||||
validateTplDocument(mockDoc, collection);
|
rule.checkLine('if true then', 'if true then', 0, ctx, []);
|
||||||
|
assert.strictEqual(ctx.openControlBlocks.length, 1);
|
||||||
|
assert.strictEqual(ctx.openControlBlocks[0].type, 'if');
|
||||||
|
|
||||||
const diags = collection.get(mockDoc.uri);
|
// Schließt falschen Block (end for statt end if)
|
||||||
|
rule.checkLine('end for;', 'end for;', 1, ctx, []);
|
||||||
// We check language-agnostically: Is there an Error exactly on line 2 (the 'end for;')?
|
assert.strictEqual(ctx.diagnostics.length, 1);
|
||||||
const hasErrorOnEndFor = diags?.some(d =>
|
assert.ok(ctx.diagnostics[0].message.includes("Falsches Ende"));
|
||||||
d.range.start.line === 2 &&
|
|
||||||
d.severity === vscode.DiagnosticSeverity.Error
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.strictEqual(hasErrorOnEndFor, true, "Need to throw an error on 'end for;' line");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Loading…
Reference in New Issue