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

261 lines
12 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';
// --- Konstanten ---
const SUPPORTED_TPL_VERSION = 23; // TPL 1.23
const warnedDocuments = new Set<string>();
// Erlaubte Phasen in einem Pattern (strikte Reihenfolge laut BMC TPL)
enum PatternPhase {
None = 0,
Metadata = 1,
Overview = 2,
Constants = 3,
Triggers = 4,
Body = 5
}
export function activate(context: vscode.ExtensionContext) {
// 1. Diagnostic Collection (Der Scanner für rote/gelbe Wellenlinien)
const diagnosticCollection = vscode.languages.createDiagnosticCollection('tpl-scanner');
context.subscriptions.push(diagnosticCollection);
// 2. Events registrieren (Ausführen beim Öffnen und Ändern)
vscode.workspace.onDidOpenTextDocument(doc => validateTplDocument(doc, diagnosticCollection));
vscode.workspace.onDidChangeTextDocument(event => validateTplDocument(event.document, diagnosticCollection));
// 3. Quick-Fix Provider (Code Actions für Glühbirnen-Menü) registrieren
context.subscriptions.push(
vscode.languages.registerCodeActionsProvider('tpl', new TplCodeActionProvider(), {
providedCodeActionKinds: [vscode.CodeActionKind.QuickFix]
})
);
// 4. Format Document Provider (Auto-Aufräumen mit Shift+Alt+F)
context.subscriptions.push(
vscode.languages.registerDocumentFormattingEditProvider('tpl', new TplDocumentFormatter())
);
}
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;
// --- State Machine Variablen ---
let inBlock: 'none' | 'pattern' | 'table' | 'definitions' = 'none';
let currentPatternPhase: PatternPhase = PatternPhase.None;
let inMultiLineComment = false;
// State Tracking für Quick-Fixes
let currentPatternName = "";
let foundMetadata = { author: false, publisher: false, origin: false, tree_path: false };
let metadataBlockLine = -1;
let bodyLineCount = 0;
// --- 1. Versions-Check im Header ---
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 (Fehler werden zu Warnungen).`);
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;
}
const diag = new vscode.Diagnostic(new vscode.Range(lineIdx, start, lineIdx, end), msg, severity);
diagnostics.push(diag);
return diag;
}
// --- 2. Der Haupt-Scanner (Zeile für Zeile) ---
for (let i = 0; i < lines.length; i++) {
const rawLine = lines[i];
const line = rawLine.trim();
// --- Tab-Check (Strikte 3-Spaces-Regel) ---
const tabIndex = rawLine.indexOf('\t');
if (tabIndex !== -1) {
const diag = addDiag(
i,
tabIndex,
rawLine.length,
"Tabs sind hier nicht erlaubt! Bitte nutze exakt 3 Spaces für Einrückungen. (Tipp: Mit 'Alt + Shift' + Mausklick/Pfeiltasten kannst du mehrere Zeilen gleichzeitig markieren und korrigieren).",
vscode.DiagnosticSeverity.Warning
);
diag.code = 'invalid_tab';
}
// Kommentare überspringen (aber Regex für Semikolon-Check braucht MultiLine-Info)
if (line.startsWith('//')) { continue; }
if (line.startsWith('/*')) { inMultiLineComment = true; continue; }
if (inMultiLineComment) {
if (line.includes('*/')) { inMultiLineComment = false; }
continue;
}
if (line.length === 0) { continue; }
// --- Semikolon-Check ---
const noSemicolonNeeded = /^(pattern|table|metadata|overview|constants|triggers|body|end\s+|if\s+|else|elif\s+|for\s+|while\s+|\/\/|\/\*|\*)/;
if (!noSemicolonNeeded.test(line)) {
if (!line.endsWith(';') && !line.endsWith('{') && !line.endsWith(',')) {
addDiag(i, rawLine.length - 1 < 0 ? 0 : rawLine.length - 1, rawLine.length, "Fehlendes Semikolon am Ende der Anweisung.", vscode.DiagnosticSeverity.Error);
}
}
// Modul-Definition prüfen (Muss als Erstes stehen)
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;
}
}
// --- Block-Einstiege (Pattern & Table) ---
if (line.startsWith('pattern ')) {
if ((inBlock as string) !== 'none') { addDiag(i, 0, 7, "Ein Pattern kann nicht innerhalb eines anderen Blocks deklariert werden.", vscode.DiagnosticSeverity.Error); }
inBlock = 'pattern';
currentPatternPhase = PatternPhase.None;
const nameMatch = line.match(/pattern\s+([a-zA-Z0-9_]+)\s+\d+\.\d+/);
if (nameMatch) {
currentPatternName = nameMatch[1];
} else {
addDiag(i, 0, rawLine.length, "Ungültige Pattern-Deklaration. Erwartet: 'pattern <Name> <Version>'", vscode.DiagnosticSeverity.Error);
}
foundMetadata = { author: false, publisher: false, origin: false, tree_path: false };
metadataBlockLine = -1;
bodyLineCount = 0;
continue;
}
if (line.startsWith('table ')) {
if ((inBlock as string) !== 'none') { addDiag(i, 0, 5, "Eine Tabelle kann nicht innerhalb eines anderen Blocks deklariert werden.", vscode.DiagnosticSeverity.Error); }
inBlock = 'table';
if (!line.match(/table\s+[a-zA-Z0-9_]+\s+\d+\.\d+/)) {
addDiag(i, 0, rawLine.length, "Ungültige Tabellen-Deklaration. Erwartet: 'table <Name> <Version>'", vscode.DiagnosticSeverity.Error);
}
continue;
}
// --- Innerhalb eines Patterns ---
if ((inBlock as string) === 'pattern') {
const checkPhase = (phaseName: string, phaseEnum: PatternPhase) => {
if (line.startsWith(phaseName)) {
if ((currentPatternPhase as PatternPhase) >= phaseEnum) {
addDiag(i, 0, phaseName.length, `'${phaseName}' Block ist hier nicht erlaubt. Die Reihenfolge ist: metadata -> overview -> constants -> triggers -> body.`, vscode.DiagnosticSeverity.Error);
}
if ((currentPatternPhase as PatternPhase) === PatternPhase.Metadata && phaseEnum > PatternPhase.Metadata) {
const missing = Object.keys(foundMetadata).filter(k => !(foundMetadata as any)[k]);
if (missing.length > 0 && metadataBlockLine !== -1) {
const diag = addDiag(metadataBlockLine, 0, 8, `Fehlende Pflicht-Metadaten: ${missing.join(', ')}`, vscode.DiagnosticSeverity.Error);
diag.code = 'missing_metadata';
}
}
currentPatternPhase = phaseEnum;
if (phaseEnum === PatternPhase.Metadata) { metadataBlockLine = i; }
}
};
checkPhase('metadata', PatternPhase.Metadata);
checkPhase('overview', PatternPhase.Overview);
checkPhase('constants', PatternPhase.Constants);
checkPhase('triggers', PatternPhase.Triggers);
checkPhase('body', PatternPhase.Body);
if ((currentPatternPhase as PatternPhase) === PatternPhase.Metadata) {
if (line.startsWith('author ')) { foundMetadata.author = true; }
if (line.startsWith('publisher ')) { foundMetadata.publisher = true; }
if (line.startsWith('origin ')) { foundMetadata.origin = true; }
if (line.startsWith('tree_path ')) { foundMetadata.tree_path = true; }
}
if ((currentPatternPhase as PatternPhase) === PatternPhase.Body && !line.startsWith('body')) {
bodyLineCount++;
if (bodyLineCount === 1) {
const expectedLog = `log.debug("Pattern start ${currentPatternName}");`;
if (!line.includes(`log.debug("Pattern start ${currentPatternName}`)) {
const diag = addDiag(i, 0, rawLine.length, `Body muss mit dem Log-Statement beginnen: ${expectedLog}`, vscode.DiagnosticSeverity.Error);
diag.code = 'missing_log_start';
diag.source = expectedLog;
}
}
}
}
// --- Innerhalb einer Tabelle ---
if ((inBlock as string) === 'table') {
if (!line.startsWith('end ') && !line.includes('table ')) {
if (!line.includes('->') && !line.includes(':=')) {
addDiag(i, 0, rawLine.length, "Ungültiger Tabelleneintrag. Nutze '[\"Key\"] -> \"Value\";' oder 'default := \"Value\";'", vscode.DiagnosticSeverity.Error);
}
}
}
// --- Allgemeine Syntax-Checks ---
const eqMatch = rawLine.match(/(?<![<>=!])=(?![=])/);
if (eqMatch && eqMatch.index) {
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.includes('?') && line.includes(':')) {
if (tplVersion < 15) {
addDiag(i, 0, rawLine.length, `Ternärer Operator (? :) erfordert mindestens TPL 1.15 (Aktuell: 1.${tplVersion}).`, vscode.DiagnosticSeverity.Error);
}
}
const streamMatch = rawLine.match(/\bstream\b/);
if (streamMatch && streamMatch.index !== undefined) {
if (tplVersion < 20) {
addDiag(i, streamMatch.index, streamMatch.index + 6, `Das 'stream'-Keyword erfordert mindestens TPL 1.20.`, vscode.DiagnosticSeverity.Error);
}
}
const notInMatch = rawLine.match(/\bnot\s+in\b/);
if (notInMatch && notInMatch.index !== undefined) {
if (tplVersion < 18) {
addDiag(i, notInMatch.index, notInMatch.index + 6, `'not in' erfordert mindestens TPL 1.18.`, vscode.DiagnosticSeverity.Error);
}
}
// --- Block-Ausstiege ---
if (line.startsWith('end pattern')) {
if ((inBlock as string) !== 'pattern') {
addDiag(i, 0, 11, "'end pattern' gefunden, aber es wurde kein Pattern gestartet.", vscode.DiagnosticSeverity.Error);
} else if ((currentPatternPhase as PatternPhase) < PatternPhase.Body) {
addDiag(i, 0, 11, "Dem Pattern fehlt ein 'body'-Block.", vscode.DiagnosticSeverity.Error);
}
inBlock = 'none';
}
else if (line.startsWith('end table')) {
if ((inBlock as string) !== 'table') {
addDiag(i, 0, 9, "'end table' gefunden, aber es wurde keine Tabelle gestartet.", vscode.DiagnosticSeverity.Error);
}
inBlock = 'none';
}
}
collection.set(document.uri, diagnostics);
}