diff --git a/.gitea/workflows/nightly.yml b/.gitea/workflows/nightly.yml new file mode 100644 index 0000000..4c54b7d --- /dev/null +++ b/.gitea/workflows/nightly.yml @@ -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 diff --git a/package.json b/package.json index 8610ed9..b29d5d1 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "default": 23, "description": "Current TPL-Subversion (e. g. 23 for TPL 1.23). Used to find old Modules and for auto generating the sceleton." }, - "tpl.rules.requireBodyLog": { + "tpl.rules.requireBodyLog": { "type": "boolean", "default": true, "description": "Require a log.debug statement at the start of a pattern's body phase." @@ -133,13 +133,14 @@ "watch": "npm-run-all -p watch:*", "watch:esbuild": "node esbuild.js --watch", "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", "watch-tests": "tsc -p . -w --outDir out", "pretest": "npm run compile-tests && npm run compile && npm run lint", "check-types": "tsc --noEmit", "lint": "eslint src", - "test": "vscode-test" + "test": "vscode-test", + "generate-builtins": "python scripts/generate_builtins.py ." }, "devDependencies": { "@types/vscode": "^1.140.0", diff --git a/src/core/TplLookup.ts b/src/core/TplLookup.ts index 1234178..61be6c6 100644 --- a/src/core/TplLookup.ts +++ b/src/core/TplLookup.ts @@ -12,7 +12,7 @@ export class TplLookup { if (coreKey) { const methods = (tplBuiltIns as any)[coreKey]; 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.) @@ -20,7 +20,7 @@ export class TplLookup { if (tkuKey) { const methods = (generatedTplBuiltIns as any)[tkuKey]; const mKey = Object.keys(methods).find(k => k.toLowerCase() === fnLower); - if (mKey) return methods[mKey]; + if (mKey) {return methods[mKey];} } return null; @@ -31,10 +31,10 @@ export class TplLookup { const nsLower = namespace.toLowerCase(); 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); - if (tkuKey) return (generatedTplBuiltIns as any)[tkuKey]; + if (tkuKey) {return (generatedTplBuiltIns as any)[tkuKey];} return null; } diff --git a/src/core/WorkspaceIndex.ts b/src/core/WorkspaceIndex.ts index 45ab266..cc9a1e9 100644 --- a/src/core/WorkspaceIndex.ts +++ b/src/core/WorkspaceIndex.ts @@ -16,7 +16,7 @@ export class WorkspaceIndex { private moduleDefinitions = new Map>(); public updateFile(uri: vscode.Uri, text: string) { - if (!uri.fsPath.endsWith('.tpl')) return; + if (!uri.fsPath.endsWith('.tpl')) {return;} const fsPath = uri.fsPath; const lines = text.split('\n'); @@ -64,7 +64,7 @@ export class WorkspaceIndex { 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*end\s+define/)) {break;} if (lines[j].match(/^\s*return\b/)) { hasReturnValue = true; break; diff --git a/src/core/rules/ParameterRule.ts b/src/core/rules/ParameterRule.ts index 85093be..1c079a5 100644 --- a/src/core/rules/ParameterRule.ts +++ b/src/core/rules/ParameterRule.ts @@ -65,10 +65,10 @@ export class ParameterRule implements LinterRule { 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. - 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; let match; @@ -78,12 +78,12 @@ export class ParameterRule implements LinterRule { const callStart = match.index; 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 parenEnd = -1; for (let i = parenStart; i < cleanLine.length; i++) { - if (cleanLine[i] === '(') depth++; + if (cleanLine[i] === '(') {depth++;} else if (cleanLine[i] === ')') { depth--; if (depth === 0) { @@ -101,9 +101,9 @@ export class ParameterRule implements LinterRule { 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++; + if (paramsStr[i] === '(' || paramsStr[i] === '[') {paramDepth++;} + else if (paramsStr[i] === ')' || paramsStr[i] === ']') {paramDepth--;} + else if (paramsStr[i] === ',' && paramDepth === 0) {commas++;} } paramCount = commas + 1; } diff --git a/src/providers/TplDefinitionProvider.ts b/src/providers/TplDefinitionProvider.ts index cb9073c..b3e0126 100644 --- a/src/providers/TplDefinitionProvider.ts +++ b/src/providers/TplDefinitionProvider.ts @@ -4,7 +4,7 @@ import { workspaceIndex } from '../core/WorkspaceIndex'; export class TplDefinitionProvider implements vscode.DefinitionProvider { public async provideDefinition(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { const range = document.getWordRangeAtPosition(position, /[a-zA-Z0-9_.]+/); - if (!range) return null; + if (!range) {return null;} const word = document.getText(range); const parts = word.split('.'); diff --git a/src/providers/TplHoverProvider.ts b/src/providers/TplHoverProvider.ts index 33ecd91..8eedc9d 100644 --- a/src/providers/TplHoverProvider.ts +++ b/src/providers/TplHoverProvider.ts @@ -5,7 +5,7 @@ import { workspaceIndex } from '../core/WorkspaceIndex'; export class TplHoverProvider implements vscode.HoverProvider { public provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): vscode.ProviderResult { const range = document.getWordRangeAtPosition(position, /[a-zA-Z0-9_.]+/); - if (!range) return null; + if (!range) {return null;} const word = document.getText(range); const parts = word.split('.'); diff --git a/src/providers/formatters/IndentationRules.ts b/src/providers/formatters/IndentationRules.ts index 5abee76..fc88069 100644 --- a/src/providers/formatters/IndentationRules.ts +++ b/src/providers/formatters/IndentationRules.ts @@ -2,10 +2,8 @@ import { FormatState } from './FormatState'; export class IndentationRules { 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 inString = false; let stringChar = ''; @@ -13,7 +11,7 @@ export class IndentationRules { for (let i = 0; i < line.length; 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) { inTripleQuote = true; stringChar = char; cleanLine += char+char+char; i += 2; continue; @@ -25,7 +23,7 @@ export class IndentationRules { continue; } if (inString) { - if (char === '\\') i++; else if (char === stringChar) inString = false; + if (char === '\\') {i++;} else if (char === stringChar) {inString = false;} continue; } if (!inString && !inTripleQuote && (char === '"' || char === "'")) { @@ -35,47 +33,40 @@ export class IndentationRules { } cleanLine = cleanLine.trim(); - // Alle vorhandenen falschen Leerzeichen am Anfang (2, 3, 6, etc.) brutal abschneiden const trimmedLine = line.trimStart(); - // 3. DEDENT: Aktuelle Zeile rückt nach links (end, elif, else) + // 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. DIE EINZIGE WAHRHEIT: Harte Spaces setzen + // 4. SET SPACES let result = trimmedLine; if (state.parenAlignments && state.parenAlignments.length > 0) { - // Visual Alignment (Runde Klammern) - Das fasst diese Regel nicht an, - // das übernimmt die nachfolgende AlignmentRule. + // Visual Alignment greift später } else if (state.inImportBlock) { - // Im Multi-Line Import rücken wir zur besseren Lesbarkeit eine Ebene tiefer ein if (!cleanLine.startsWith('from ')) { result = " ".repeat((state.indentLevel + 2) * state.tabSize) + trimmedLine; } else { result = state.currentIndentString + trimmedLine; } } else { - // Standard-Einrückung unter Nutzung deines Getters result = state.currentIndentString + trimmedLine; } - // 5. INDENT: Nächste Zeile rückt nach rechts + // 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++; - } - if (/\b(then|do)\b$/.test(cleanLine)) { + } else if (/\b(then|do)\b$/.test(cleanLine)) { state.indentLevel++; - } - // elif/else eröffnen sofort wieder einen neuen Block für die Folgezeile - if (/^(elif|else)\b/.test(cleanLine)) { + } else if (/^else\b/.test(cleanLine)) { state.indentLevel++; } - // 6. Multi-Line Import Tracking + // 6. Import Block Tracking if (cleanLine.startsWith('from ')) { state.inImportBlock = true; } diff --git a/src/test/fixtures/format_bad.tpl b/src/test/fixtures/format_bad.tpl new file mode 100644 index 0000000..63a1ce0 --- /dev/null +++ b/src/test/fixtures/format_bad.tpl @@ -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; diff --git a/src/test/fixtures/lexer_edge_cases.tpl b/src/test/fixtures/lexer_edge_cases.tpl new file mode 100644 index 0000000..300dd09 --- /dev/null +++ b/src/test/fixtures/lexer_edge_cases.tpl @@ -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; diff --git a/src/test/fixtures/linter_negative.tpl b/src/test/fixtures/linter_negative.tpl new file mode 100644 index 0000000..46e6ad0 --- /dev/null +++ b/src/test/fixtures/linter_negative.tpl @@ -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; diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts new file mode 100644 index 0000000..fea7788 --- /dev/null +++ b/src/test/suite/extension.test.ts @@ -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;'); + }); + }); +});