Compare commits

..

2 Commits

12 changed files with 222 additions and 37 deletions

View File

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

View File

@ -133,13 +133,14 @@
"watch": "npm-run-all -p watch:*", "watch": "npm-run-all -p watch:*",
"watch:esbuild": "node esbuild.js --watch", "watch:esbuild": "node esbuild.js --watch",
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json", "watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"package": "npm run check-types && npm run lint && node esbuild.js --production", "package": "npm run check-types && npm run lint && npm run test && node esbuild.js --production",
"compile-tests": "tsc -p . --outDir out", "compile-tests": "tsc -p . --outDir out",
"watch-tests": "tsc -p . -w --outDir out", "watch-tests": "tsc -p . -w --outDir out",
"pretest": "npm run compile-tests && npm run compile && npm run lint", "pretest": "npm run compile-tests && npm run compile && npm run lint",
"check-types": "tsc --noEmit", "check-types": "tsc --noEmit",
"lint": "eslint src", "lint": "eslint src",
"test": "vscode-test" "test": "vscode-test",
"generate-builtins": "python scripts/generate_builtins.py ."
}, },
"devDependencies": { "devDependencies": {
"@types/vscode": "^1.140.0", "@types/vscode": "^1.140.0",

View File

@ -12,7 +12,7 @@ export class TplLookup {
if (coreKey) { if (coreKey) {
const methods = (tplBuiltIns as any)[coreKey]; const methods = (tplBuiltIns as any)[coreKey];
const mKey = Object.keys(methods).find(k => k.toLowerCase() === fnLower); const mKey = Object.keys(methods).find(k => k.toLowerCase() === fnLower);
if (mKey) return methods[mKey]; if (mKey) {return methods[mKey];}
} }
// 2. Suche in Generierten TKU Built-Ins (rdbms_functions, etc.) // 2. Suche in Generierten TKU Built-Ins (rdbms_functions, etc.)
@ -20,7 +20,7 @@ export class TplLookup {
if (tkuKey) { if (tkuKey) {
const methods = (generatedTplBuiltIns as any)[tkuKey]; const methods = (generatedTplBuiltIns as any)[tkuKey];
const mKey = Object.keys(methods).find(k => k.toLowerCase() === fnLower); const mKey = Object.keys(methods).find(k => k.toLowerCase() === fnLower);
if (mKey) return methods[mKey]; if (mKey) {return methods[mKey];}
} }
return null; return null;
@ -31,10 +31,10 @@ export class TplLookup {
const nsLower = namespace.toLowerCase(); const nsLower = namespace.toLowerCase();
const coreKey = Object.keys(tplBuiltIns).find(k => k.toLowerCase() === nsLower); const coreKey = Object.keys(tplBuiltIns).find(k => k.toLowerCase() === nsLower);
if (coreKey) return (tplBuiltIns as any)[coreKey]; if (coreKey) {return (tplBuiltIns as any)[coreKey];}
const tkuKey = Object.keys(generatedTplBuiltIns).find(k => k.toLowerCase() === nsLower); const tkuKey = Object.keys(generatedTplBuiltIns).find(k => k.toLowerCase() === nsLower);
if (tkuKey) return (generatedTplBuiltIns as any)[tkuKey]; if (tkuKey) {return (generatedTplBuiltIns as any)[tkuKey];}
return null; return null;
} }

View File

@ -16,7 +16,7 @@ export class WorkspaceIndex {
private moduleDefinitions = new Map<string, Set<string>>(); private moduleDefinitions = new Map<string, Set<string>>();
public updateFile(uri: vscode.Uri, text: string) { public updateFile(uri: vscode.Uri, text: string) {
if (!uri.fsPath.endsWith('.tpl')) return; if (!uri.fsPath.endsWith('.tpl')) {return;}
const fsPath = uri.fsPath; const fsPath = uri.fsPath;
const lines = text.split('\n'); const lines = text.split('\n');
@ -64,7 +64,7 @@ export class WorkspaceIndex {
let hasReturnValue = false; let hasReturnValue = false;
for (let j = i + 1; j < lines.length; j++) { for (let j = i + 1; j < lines.length; j++) {
if (lines[j].match(/^\s*end\s+define/)) break; if (lines[j].match(/^\s*end\s+define/)) {break;}
if (lines[j].match(/^\s*return\b/)) { if (lines[j].match(/^\s*return\b/)) {
hasReturnValue = true; hasReturnValue = true;
break; break;

View File

@ -65,10 +65,10 @@ export class ParameterRule implements LinterRule {
cleanLine += char; cleanLine += char;
} }
if (cleanLine.trim().length === 0) return; if (cleanLine.trim().length === 0) {return;}
// Die Definition der Funktion selbst ist kein Aufruf und wird ignoriert. // Die Definition der Funktion selbst ist kein Aufruf und wird ignoriert.
if (/^\s*define\s+/.test(cleanLine)) return; if (/^\s*define\s+/.test(cleanLine)) {return;}
const callRegex = /\b([a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)?)\s*\(/g; const callRegex = /\b([a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)?)\s*\(/g;
let match; let match;
@ -78,12 +78,12 @@ export class ParameterRule implements LinterRule {
const callStart = match.index; const callStart = match.index;
const parenStart = callStart + funcName.length; const parenStart = callStart + funcName.length;
if (/^(if|elif|where|for|while|in|matches|has|subword|defined)\b/.test(funcName)) continue; if (/^(if|elif|where|for|while|in|matches|has|subword|defined)\b/.test(funcName)) {continue;}
let depth = 0; let depth = 0;
let parenEnd = -1; let parenEnd = -1;
for (let i = parenStart; i < cleanLine.length; i++) { for (let i = parenStart; i < cleanLine.length; i++) {
if (cleanLine[i] === '(') depth++; if (cleanLine[i] === '(') {depth++;}
else if (cleanLine[i] === ')') { else if (cleanLine[i] === ')') {
depth--; depth--;
if (depth === 0) { if (depth === 0) {
@ -101,9 +101,9 @@ export class ParameterRule implements LinterRule {
let paramDepth = 0; let paramDepth = 0;
let commas = 0; let commas = 0;
for (let i = 0; i < paramsStr.length; i++) { for (let i = 0; i < paramsStr.length; i++) {
if (paramsStr[i] === '(' || paramsStr[i] === '[') paramDepth++; if (paramsStr[i] === '(' || paramsStr[i] === '[') {paramDepth++;}
else if (paramsStr[i] === ')' || paramsStr[i] === ']') paramDepth--; else if (paramsStr[i] === ')' || paramsStr[i] === ']') {paramDepth--;}
else if (paramsStr[i] === ',' && paramDepth === 0) commas++; else if (paramsStr[i] === ',' && paramDepth === 0) {commas++;}
} }
paramCount = commas + 1; paramCount = commas + 1;
} }

View File

@ -4,7 +4,7 @@ import { workspaceIndex } from '../core/WorkspaceIndex';
export class TplDefinitionProvider implements vscode.DefinitionProvider { export class TplDefinitionProvider implements vscode.DefinitionProvider {
public async provideDefinition(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Definition | null> { 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_.]+/); 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 parts = word.split('.');

View File

@ -5,7 +5,7 @@ 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 provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): vscode.ProviderResult<vscode.Hover> {
const range = document.getWordRangeAtPosition(position, /[a-zA-Z0-9_.]+/); const range = document.getWordRangeAtPosition(position, /[a-zA-Z0-9_.]+/);
if (!range) return null; if (!range) {return null;}
const word = document.getText(range); const word = document.getText(range);
const parts = word.split('.'); const parts = word.split('.');

View File

@ -2,10 +2,8 @@ import { FormatState } from './FormatState';
export class IndentationRules { export class IndentationRules {
public apply(line: string, state: FormatState): string { public apply(line: string, state: FormatState): string {
// 1. Leere Zeilen komplett rasieren if (line.trim().length === 0) {return "";}
if (line.trim().length === 0) return "";
// 2. Strings und Kommentare sicher entfernen (für die Keyword-Erkennung)
let cleanLine = ""; let cleanLine = "";
let inString = false; let inString = false;
let stringChar = ''; let stringChar = '';
@ -13,7 +11,7 @@ export class IndentationRules {
for (let i = 0; i < line.length; i++) { for (let i = 0; i < line.length; i++) {
const char = line[i]; const char = line[i];
if (!inString && !inTripleQuote && char === '/' && line[i+1] === '/') break; if (!inString && !inTripleQuote && char === '/' && line[i+1] === '/') {break;}
if (!inString && !inTripleQuote && (char === '"' || char === "'") && line[i+1] === char && line[i+2] === char) { if (!inString && !inTripleQuote && (char === '"' || char === "'") && line[i+1] === char && line[i+2] === char) {
inTripleQuote = true; stringChar = char; cleanLine += char+char+char; i += 2; continue; inTripleQuote = true; stringChar = char; cleanLine += char+char+char; i += 2; continue;
@ -25,7 +23,7 @@ export class IndentationRules {
continue; continue;
} }
if (inString) { if (inString) {
if (char === '\\') i++; else if (char === stringChar) inString = false; if (char === '\\') {i++;} else if (char === stringChar) {inString = false;}
continue; continue;
} }
if (!inString && !inTripleQuote && (char === '"' || char === "'")) { if (!inString && !inTripleQuote && (char === '"' || char === "'")) {
@ -35,47 +33,40 @@ export class IndentationRules {
} }
cleanLine = cleanLine.trim(); cleanLine = cleanLine.trim();
// Alle vorhandenen falschen Leerzeichen am Anfang (2, 3, 6, etc.) brutal abschneiden
const trimmedLine = line.trimStart(); const trimmedLine = line.trimStart();
// 3. DEDENT: Aktuelle Zeile rückt nach links (end, elif, else) // 3. DEDENT
if (/^end\b/.test(cleanLine)) { if (/^end\b/.test(cleanLine)) {
state.indentLevel = Math.max(0, state.indentLevel - 1); state.indentLevel = Math.max(0, state.indentLevel - 1);
} else if (/^(elif|else)\b/.test(cleanLine)) { } else if (/^(elif|else)\b/.test(cleanLine)) {
state.indentLevel = Math.max(0, state.indentLevel - 1); state.indentLevel = Math.max(0, state.indentLevel - 1);
} }
// 4. DIE EINZIGE WAHRHEIT: Harte Spaces setzen // 4. SET SPACES
let result = trimmedLine; let result = trimmedLine;
if (state.parenAlignments && state.parenAlignments.length > 0) { if (state.parenAlignments && state.parenAlignments.length > 0) {
// Visual Alignment (Runde Klammern) - Das fasst diese Regel nicht an, // Visual Alignment greift später
// das übernimmt die nachfolgende AlignmentRule.
} else if (state.inImportBlock) { } else if (state.inImportBlock) {
// Im Multi-Line Import rücken wir zur besseren Lesbarkeit eine Ebene tiefer ein
if (!cleanLine.startsWith('from ')) { if (!cleanLine.startsWith('from ')) {
result = " ".repeat((state.indentLevel + 2) * state.tabSize) + trimmedLine; result = " ".repeat((state.indentLevel + 2) * state.tabSize) + trimmedLine;
} else { } else {
result = state.currentIndentString + trimmedLine; result = state.currentIndentString + trimmedLine;
} }
} else { } else {
// Standard-Einrückung unter Nutzung deines Getters
result = state.currentIndentString + trimmedLine; result = state.currentIndentString + trimmedLine;
} }
// 5. INDENT: Nächste Zeile rückt nach rechts // 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)) { 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)) {
if (/\b(then|do)\b$/.test(cleanLine)) {
state.indentLevel++; state.indentLevel++;
} } else if (/^else\b/.test(cleanLine)) {
// elif/else eröffnen sofort wieder einen neuen Block für die Folgezeile
if (/^(elif|else)\b/.test(cleanLine)) {
state.indentLevel++; state.indentLevel++;
} }
// 6. Multi-Line Import Tracking // 6. Import Block Tracking
if (cleanLine.startsWith('from ')) { if (cleanLine.startsWith('from ')) {
state.inImportBlock = true; state.inImportBlock = true;
} }

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

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

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

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

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

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

View File

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