bmc-tpl-code-all-around/src/extension.ts

304 lines
16 KiB
TypeScript
Raw Normal View History

2026-10-07 21:34:05 +00:00
import * as vscode from 'vscode';
import { TplCodeActionProvider } from './TplCodeActionProvider';
import { TplDocumentFormatter } from './TplDocumentFormatter';
import { TplCompletionProvider } from './TplCompletionProvider';
import { TplHoverProvider } from './TplHoverProvider';
2026-10-07 21:34:05 +00:00
const SUPPORTED_TPL_VERSION = 23;
2026-10-07 21:34:05 +00:00
const warnedDocuments = new Set<string>();
enum PatternPhase { None = 0, Metadata = 1, Overview = 2, Constants = 3, Triggers = 4, Mapping = 5, Removal = 6, Body = 7 }
2026-10-07 21:34:05 +00:00
export function activate(context: vscode.ExtensionContext) {
const diagnosticCollection = vscode.languages.createDiagnosticCollection('tpl-scanner');
context.subscriptions.push(diagnosticCollection);
vscode.workspace.onDidOpenTextDocument(doc => validateTplDocument(doc, diagnosticCollection));
vscode.workspace.onDidChangeTextDocument(event => validateTplDocument(event.document, diagnosticCollection));
context.subscriptions.push(vscode.languages.registerCodeActionsProvider('tpl', new TplCodeActionProvider(), { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] }));
context.subscriptions.push(vscode.languages.registerDocumentFormattingEditProvider('tpl', new TplDocumentFormatter()));
context.subscriptions.push(vscode.languages.registerCompletionItemProvider('tpl', new TplCompletionProvider(), '.'));
context.subscriptions.push(vscode.languages.registerHoverProvider('tpl', new TplHoverProvider()));
for (const doc of vscode.workspace.textDocuments) {
validateTplDocument(doc, diagnosticCollection);
}
2026-10-07 21:34:05 +00:00
}
function validateTplDocument(document: vscode.TextDocument, collection: vscode.DiagnosticCollection) {
if (document.languageId !== 'tpl') { return; }
const diagnostics: vscode.Diagnostic[] = [];
const text = document.getText();
const lines = text.split('\n');
let tplVersion = 10;
let toleranceMode = false;
let hasModuleDef = false;
let inBlock: 'none' | 'pattern' | 'table' | 'definitions' | 'enumeration' | 'configuration' = 'none';
2026-10-07 21:34:05 +00:00
let currentPatternPhase: PatternPhase = PatternPhase.None;
let inMultiLineComment = false;
let inTripleDouble = false;
let inTripleSingle = false;
let stringNeedsSemicolon = false;
2026-10-07 21:34:05 +00:00
let activePhaseName = "";
let activePhaseLine = -1;
2026-10-07 21:34:05 +00:00
let currentPatternName = "";
// Modul-Ebene Metadaten Tracking
let inModuleMetadata = false;
let hasModuleMetadata = false;
let moduleMetadataBlockLine = -1;
let foundModuleMetadata = { origin: false, author: false, products: false, publishers: false, tree_path: false };
const parenStack: number[] = [];
const bracketStack: number[] = [];
const openDefines: number[] = [];
2026-10-07 21:34:05 +00:00
const versionMatch = text.match(/tpl\s+1\.(\d+)\s+module/);
if (versionMatch) {
tplVersion = parseInt(versionMatch[1], 10);
if (tplVersion > SUPPORTED_TPL_VERSION) {
toleranceMode = true;
if (!warnedDocuments.has(document.uri.toString())) {
vscode.window.showInformationMessage(`TPL 1.${tplVersion} erkannt. Linter läuft im Toleranz-Modus.`);
2026-10-07 21:34:05 +00:00
warnedDocuments.add(document.uri.toString());
}
}
}
function addDiag(lineIdx: number, start: number, end: number, msg: string, severity: vscode.DiagnosticSeverity): vscode.Diagnostic {
if (toleranceMode && severity === vscode.DiagnosticSeverity.Error) { severity = vscode.DiagnosticSeverity.Warning; }
2026-10-07 21:34:05 +00:00
const diag = new vscode.Diagnostic(new vscode.Range(lineIdx, start, lineIdx, end), msg, severity);
diagnostics.push(diag);
return diag;
}
for (let i = 0; i < lines.length; i++) {
const rawLine = lines[i];
const line = rawLine.trim();
const tripleDoubleCount = (rawLine.match(/"""/g) || []).length;
const tripleSingleCount = (rawLine.match(/'''/g) || []).length;
let isInternalStringLine = false;
let justOpenedString = false;
let isClosingString = false;
if (inTripleDouble) {
isInternalStringLine = true;
if (tripleDoubleCount % 2 !== 0) { inTripleDouble = false; isClosingString = true; }
} else if (tripleDoubleCount % 2 !== 0) {
inTripleDouble = true; justOpenedString = true;
}
if (inTripleSingle) {
isInternalStringLine = true;
if (tripleSingleCount % 2 !== 0) { inTripleSingle = false; isClosingString = true; }
} else if (tripleSingleCount % 2 !== 0 && !justOpenedString) {
inTripleSingle = true; justOpenedString = true;
}
if (isInternalStringLine && (inTripleDouble || inTripleSingle)) { continue; }
if (justOpenedString) { stringNeedsSemicolon = !line.startsWith('"""') && !line.startsWith("'''"); }
2026-10-07 21:34:05 +00:00
const tabIndex = rawLine.indexOf('\t');
if (tabIndex !== -1) {
const diag = addDiag(i, tabIndex, tabIndex + 1, "Bitte Tabs durch Spaces ersetzen", vscode.DiagnosticSeverity.Warning);
2026-10-07 21:34:05 +00:00
diag.code = 'invalid_tab';
}
let cleanLineForParens = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, '');
const commentIdx = cleanLineForParens.indexOf('//');
if (commentIdx !== -1) { cleanLineForParens = cleanLineForParens.substring(0, commentIdx); }
for (let charIdx = 0; charIdx < cleanLineForParens.length; charIdx++) {
const char = cleanLineForParens[charIdx];
if (char === '(') { parenStack.push(i); }
else if (char === ')') {
if (parenStack.length > 0) { parenStack.pop(); }
else { addDiag(i, 0, rawLine.length, "Unerwartete schließende Klammer ')'. Es fehlt eine öffnende Klammer.", vscode.DiagnosticSeverity.Error); }
}
else if (char === '[') { bracketStack.push(i); }
else if (char === ']') {
if (bracketStack.length > 0) { bracketStack.pop(); }
else { addDiag(i, 0, rawLine.length, "Unerwartete schließende eckige Klammer ']'. Es fehlt eine öffnende Klammer.", vscode.DiagnosticSeverity.Error); }
}
2026-10-07 21:34:05 +00:00
}
if (line.startsWith('//') || line.startsWith('/*')) { if (line.startsWith('/*')) inMultiLineComment = true; continue; }
if (inMultiLineComment) { if (line.includes('*/')) inMultiLineComment = false; continue; }
2026-10-07 21:34:05 +00:00
if (line.length === 0) { continue; }
if (isClosingString) {
if (stringNeedsSemicolon && !line.endsWith(';')) {
addDiag(i, rawLine.length - 1 < 0 ? 0 : rawLine.length - 1, rawLine.length, "Fehlendes Semikolon am Ende der Zuweisung.", vscode.DiagnosticSeverity.Error);
}
} else {
const noSemicolonNeeded = /^(pattern|syncmapping|table|enumeration|configuration|definitions?|define|metadata|overview|constants|triggers|mapping|removal|body|end\s+|if\s+|else|elif\s+|for\s+|while\s+|\/\/|\/\*|\*)/;
// Enumerations benötigen keine Semikolons
if (!noSemicolonNeeded.test(line) && !justOpenedString && parenStack.length === 0 && bracketStack.length === 0 && inBlock !== 'enumeration') {
const lineToCheck = line.split('//')[0].trim();
if (!lineToCheck.endsWith(';') && !lineToCheck.endsWith('{') && !lineToCheck.endsWith(',')) {
addDiag(i, rawLine.length - 1 < 0 ? 0 : rawLine.length - 1, rawLine.length, "Fehlendes Semikolon am Ende der Anweisung.", vscode.DiagnosticSeverity.Error);
}
2026-10-07 21:34:05 +00:00
}
}
if (!hasModuleDef) {
if (line.startsWith('tpl ')) { hasModuleDef = true; }
else { addDiag(i, 0, rawLine.length, "Jede Datei muss mit 'tpl 1.x module <name>;' beginnen.", vscode.DiagnosticSeverity.Error); hasModuleDef = true; }
}
// --- Modul-Metadaten Logik ---
if (line.startsWith('metadata') && inBlock === 'none') {
inModuleMetadata = true;
hasModuleMetadata = true;
moduleMetadataBlockLine = i;
continue;
}
if (inModuleMetadata) {
if (line.match(/^origin\b/)) { foundModuleMetadata.origin = true; }
if (line.match(/^author\b/)) { foundModuleMetadata.author = true; }
if (line.match(/^products\b/)) { foundModuleMetadata.products = true; }
if (line.match(/^publishers\b/)) { foundModuleMetadata.publishers = true; }
if (line.match(/^tree_path\b/)) { foundModuleMetadata.tree_path = true; }
if (line.startsWith('end metadata')) {
inModuleMetadata = false;
const missing = Object.keys(foundModuleMetadata).filter(k => !(foundModuleMetadata as any)[k]);
if (missing.length > 0 && moduleMetadataBlockLine !== -1) {
const diag = addDiag(moduleMetadataBlockLine, 0, 8, `Fehlende Pflicht-Metadaten (Modul-Ebene): ${missing.join(', ')}`, vscode.DiagnosticSeverity.Error);
diag.code = 'missing_module_metadata';
}
2026-10-07 21:34:05 +00:00
}
continue;
2026-10-07 21:34:05 +00:00
}
// Block checks
if (line.match(/^(pattern|syncmapping|table|definitions?|enumeration|configuration)\b/)) {
if (!hasModuleMetadata && inBlock === 'none') {
addDiag(i, 0, line.length, "Ein 'metadata' Block auf Modul-Ebene ist Pflicht, bevor andere Blöcke definiert werden.", vscode.DiagnosticSeverity.Error);
}
}
if (line.startsWith('definitions ') || line.startsWith('definition ')) {
if (inBlock !== 'none') { addDiag(i, 0, line.length, "Ein Definitions-Block muss auf oberster Ebene deklariert werden.", vscode.DiagnosticSeverity.Error); }
inBlock = 'definitions'; continue;
}
if (line.startsWith('define ')) { openDefines.push(i); continue; }
if (line.startsWith('enumeration ')) {
if (inBlock !== 'none') { addDiag(i, 0, line.length, "Eine Enumeration muss auf oberster Ebene deklariert werden.", vscode.DiagnosticSeverity.Error); }
inBlock = 'enumeration'; continue;
}
if (line.startsWith('configuration ')) {
if (inBlock !== 'none') { addDiag(i, 0, line.length, "Eine Konfiguration muss auf oberster Ebene deklariert werden.", vscode.DiagnosticSeverity.Error); }
inBlock = 'configuration'; continue;
}
if (line.startsWith('pattern ') || line.startsWith('syncmapping ')) {
if (inBlock !== 'none') { addDiag(i, 0, 7, "Ein Pattern/Syncmapping kann nicht innerhalb eines anderen Blocks deklariert werden.", vscode.DiagnosticSeverity.Error); }
2026-10-07 21:34:05 +00:00
inBlock = 'pattern';
currentPatternPhase = PatternPhase.None;
activePhaseName = "";
activePhaseLine = -1;
2026-10-07 21:34:05 +00:00
const nameMatch = line.match(/(pattern|syncmapping)\s+([a-zA-Z0-9_]+)\s+\d+\.\d+/);
if (nameMatch) { currentPatternName = nameMatch[2]; }
else { addDiag(i, 0, rawLine.length, "Ungültige Pattern-Deklaration. Erwartet: 'pattern <Name> <Version>'", vscode.DiagnosticSeverity.Error); }
2026-10-07 21:34:05 +00:00
continue;
}
if (line.startsWith('table ')) {
if (inBlock !== 'none') { addDiag(i, 0, 5, "Eine Tabelle kann nicht innerhalb eines anderen Blocks deklariert werden.", vscode.DiagnosticSeverity.Error); }
inBlock = 'table'; continue;
2026-10-07 21:34:05 +00:00
}
if (inBlock === 'pattern') {
2026-10-07 21:34:05 +00:00
const checkPhase = (phaseName: string, phaseEnum: PatternPhase) => {
if (line.startsWith(phaseName)) {
if (activePhaseName !== "") { addDiag(activePhaseLine, 0, activePhaseName.length, `Block '${activePhaseName}' wurde nicht mit 'end ${activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); }
if (currentPatternPhase >= phaseEnum) { addDiag(i, 0, phaseName.length, `'${phaseName}' Block ist hier nicht erlaubt.`, vscode.DiagnosticSeverity.Error); }
2026-10-07 21:34:05 +00:00
currentPatternPhase = phaseEnum;
activePhaseName = phaseName;
activePhaseLine = i;
2026-10-07 21:34:05 +00:00
}
};
checkPhase('metadata', PatternPhase.Metadata);
checkPhase('overview', PatternPhase.Overview);
checkPhase('constants', PatternPhase.Constants);
checkPhase('triggers', PatternPhase.Triggers);
checkPhase('mapping', PatternPhase.Mapping);
checkPhase('removal', PatternPhase.Removal);
if (line.startsWith('body')) {
if (activePhaseName !== "") { addDiag(activePhaseLine, 0, activePhaseName.length, `Block '${activePhaseName}' wurde nicht mit 'end ${activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); }
activePhaseName = "body";
activePhaseLine = i;
let foundLog = false;
for (let j = i + 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(currentPatternName)) { foundLog = true; break; }
}
2026-10-07 21:34:05 +00:00
if (!foundLog) {
const expectedLog = `log.debug("Start pattern ${currentPatternName}");`;
const diag = addDiag(i, 0, rawLine.length, `Im Body fehlt das Log-Statement mit dem Pattern-Namen. Bsp: ${expectedLog}`, vscode.DiagnosticSeverity.Error);
diag.code = 'missing_log_start'; diag.source = expectedLog;
2026-10-07 21:34:05 +00:00
}
}
const closers = ['end metadata', 'end overview', 'end constants', 'end triggers', 'end mapping', 'end removal', 'end body'];
for (const closer of closers) {
if (line.startsWith(closer)) {
const phase = closer.replace('end ', '').replace(';', '').trim();
if (activePhaseName !== phase) { addDiag(i, 0, line.length, `Unerwartetes '${closer}'. Erwartet wurde 'end ${activePhaseName};'.`, vscode.DiagnosticSeverity.Error); }
else { activePhaseName = ""; }
2026-10-07 21:34:05 +00:00
}
}
}
const eqMatch = rawLine.match(/(?<![<>=!:])=(?![=])/);
if (eqMatch && eqMatch.index && !justOpenedString) {
2026-10-07 21:34:05 +00:00
if (!line.includes('"') && !line.includes("'")) {
addDiag(i, eqMatch.index, eqMatch.index + 1, "Zuweisung in TPL erfolgt mit ':=', nicht mit '='. '=' ist für Vergleiche.", vscode.DiagnosticSeverity.Error);
}
}
if (line.startsWith('end pattern') || line.startsWith('end syncmapping')) {
if (activePhaseName !== "") { addDiag(activePhaseLine, 0, activePhaseName.length, `Block '${activePhaseName}' wurde nicht mit 'end ${activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); }
if (inBlock !== 'pattern') { addDiag(i, 0, 11, "'end pattern' gefunden, ohne offenen Pattern-Block.", vscode.DiagnosticSeverity.Error); }
inBlock = 'none';
2026-10-07 21:34:05 +00:00
}
else if (line.startsWith('end table') || line.startsWith('end enumeration') || line.startsWith('end configuration')) {
2026-10-07 21:34:05 +00:00
inBlock = 'none';
}
else if (line.startsWith('end definitions') || line.startsWith('end definition')) {
if (inBlock !== 'definitions') { addDiag(i, 0, line.length, "'end definitions' gefunden, ohne offenen Definitions-Block.", vscode.DiagnosticSeverity.Error); }
2026-10-07 21:34:05 +00:00
inBlock = 'none';
}
else if (line.startsWith('end define')) {
if (openDefines.length > 0) { openDefines.pop(); }
else { addDiag(i, 0, line.length, "'end define' gefunden, ohne offenes 'define'.", vscode.DiagnosticSeverity.Error); }
}
2026-10-07 21:34:05 +00:00
}
for (const unclosedLine of parenStack) { addDiag(unclosedLine, 0, lines[unclosedLine].length, "Öffnende Klammer '(' wird nicht geschlossen!", vscode.DiagnosticSeverity.Error); }
for (const unclosedLine of bracketStack) { addDiag(unclosedLine, 0, lines[unclosedLine].length, "Öffnende eckige Klammer '[' wird nicht geschlossen!", vscode.DiagnosticSeverity.Error); }
for (const unclosedLine of openDefines) { addDiag(unclosedLine, 0, lines[unclosedLine].length, "Block 'define' wurde nicht mit 'end define;' geschlossen!", vscode.DiagnosticSeverity.Error); }
2026-10-07 21:34:05 +00:00
collection.set(document.uri, diagnostics);
}