261 lines
12 KiB
TypeScript
261 lines
12 KiB
TypeScript
|
|
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);
|
||
|
|
}
|