2026-10-11 02:31:12 +00:00
import * as vscode from 'vscode' ;
import { LinterRule } from './LinterRule' ;
import { LinterContext } from '../LinterContext' ;
export class SyntaxRule implements LinterRule {
2026-10-12 19:43:46 +00:00
public checkLine ( line : string , rawLine : string , lineIdx : number , ctx : LinterContext , lines : string [ ] ) : void {
2026-10-11 02:31:12 +00:00
this . checkTabs ( rawLine , lineIdx , ctx ) ;
2026-10-12 19:43:46 +00:00
// TODOs finden (blauer Info-Hinweis)
const todoMatch = rawLine . match ( /\bTODO\s*:(.*)/i ) ;
if ( todoMatch ) {
ctx . addDiag ( lineIdx , todoMatch . index ! , rawLine . length , ` TODO: ${ todoMatch [ 1 ] . trim ( ) } ` , vscode . DiagnosticSeverity . Information ) ;
}
let cleanLine = line . replace ( /"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g , '""' ) ;
const commentIdx = cleanLine . indexOf ( '//' ) ;
let codePart = commentIdx !== - 1 ? cleanLine . substring ( 0 , commentIdx ) . trim ( ) : cleanLine . trim ( ) ;
if ( codePart . length === 0 ) return ;
let isStructuralKeyword = /^(tpl|pattern|syncmapping|table|definitions?|define|enumeration|configuration|metadata|overview|constants|triggers|mapping|removal|body|end|on|tags|origin|author|products|publishers|tree_path|import|from)\b/ . test ( codePart ) ;
if ( codePart . startsWith ( '"' ) || codePart . startsWith ( "'" ) ) {
isStructuralKeyword = true ;
}
const isValidExecutionArea =
ctx . activePhaseName === 'body' ||
ctx . activePhaseName === 'triggers' ||
ctx . activePhaseName === 'constants' ||
ctx . activePhaseName === 'overview' ||
ctx . activePhaseName === 'mapping' ||
ctx . activePhaseName === 'metadata' ||
ctx . inModuleMetadata ||
ctx . inBlock === 'table' ||
ctx . inBlock === 'definitions' ||
ctx . openDefines . length > 0 ;
// 'then' und 'do' auf alleinstehenden Zeilen sind legitimer Code
if ( ! isStructuralKeyword && ! isValidExecutionArea && ! /^(then|do)\b/ . test ( codePart ) ) {
ctx . addDiag ( lineIdx , 0 , rawLine . length , "Ausführbarer Code ist hier nicht erlaubt. (Erlaubt in: 'body', 'triggers', 'constants', 'overview', 'mapping', 'metadata', 'definitions' oder 'define')." , vscode . DiagnosticSeverity . Error ) ;
}
this . checkBrackets ( codePart , rawLine , lineIdx , ctx ) ;
this . checkAssignments ( codePart , rawLine , lineIdx , ctx ) ;
this . checkSemicolons ( codePart , rawLine , lineIdx , ctx , line ) ;
2026-10-11 02:31:12 +00:00
}
public checkEnd ( lines : string [ ] , ctx : LinterContext ) : void {
for ( const unclosedLine of ctx . parenStack ) { ctx . addDiag ( unclosedLine , 0 , lines [ unclosedLine ] . length , "Öffnende Klammer '(' wird nicht geschlossen!" , vscode . DiagnosticSeverity . Error ) ; }
for ( const unclosedLine of ctx . bracketStack ) { ctx . addDiag ( unclosedLine , 0 , lines [ unclosedLine ] . length , "Öffnende eckige Klammer '[' wird nicht geschlossen!" , vscode . DiagnosticSeverity . Error ) ; }
}
private checkTabs ( rawLine : string , lineIdx : number , ctx : LinterContext ) {
const tabIndex = rawLine . indexOf ( '\t' ) ;
if ( tabIndex !== - 1 ) {
const diag = ctx . addDiag ( lineIdx , tabIndex , tabIndex + 1 , "Bitte Tabs durch Spaces ersetzen" , vscode . DiagnosticSeverity . Warning ) ;
diag . code = 'invalid_tab' ;
}
}
2026-10-12 19:43:46 +00:00
private checkBrackets ( codePart : string , rawLine : string , lineIdx : number , ctx : LinterContext ) {
for ( let charIdx = 0 ; charIdx < codePart . length ; charIdx ++ ) {
const char = codePart [ charIdx ] ;
2026-10-11 02:31:12 +00:00
if ( char === '(' ) { ctx . parenStack . push ( lineIdx ) ; }
else if ( char === ')' ) {
if ( ctx . parenStack . length > 0 ) { ctx . parenStack . pop ( ) ; }
else { ctx . addDiag ( lineIdx , 0 , rawLine . length , "Unerwartete schließende Klammer ')'. Es fehlt eine öffnende Klammer." , vscode . DiagnosticSeverity . Error ) ; }
}
else if ( char === '[' ) { ctx . bracketStack . push ( lineIdx ) ; }
else if ( char === ']' ) {
if ( ctx . bracketStack . length > 0 ) { ctx . bracketStack . pop ( ) ; }
else { ctx . addDiag ( lineIdx , 0 , rawLine . length , "Unerwartete schließende eckige Klammer ']'. Es fehlt eine öffnende Klammer." , vscode . DiagnosticSeverity . Error ) ; }
}
}
}
2026-10-12 19:43:46 +00:00
private checkAssignments ( codePart : string , rawLine : string , lineIdx : number , ctx : LinterContext ) {
if ( /\b(if|elif|where)\b/ . test ( codePart ) ) { ctx . inConditionBlock = true ; }
2026-10-11 02:31:12 +00:00
2026-10-12 19:43:46 +00:00
if ( codePart . includes ( '=' ) && ! codePart . includes ( ':=' ) && ! codePart . includes ( '>=' ) && ! codePart . includes ( '<=' ) ) {
if ( ! ctx . inConditionBlock && ctx . parenStack . length === 0 && ctx . bracketStack . length === 0 && ! /\b(matches)\b/ . test ( codePart ) ) {
const idx = rawLine . indexOf ( '=' ) ;
2026-10-11 02:31:12 +00:00
if ( idx !== - 1 ) {
ctx . addDiag ( lineIdx , idx , idx + 1 , "Falsche Zuweisung. In TPL wird ':=' für Zuweisungen genutzt." , vscode . DiagnosticSeverity . Error ) ;
}
}
}
2026-10-12 19:43:46 +00:00
if ( /\b(then|do)\b/ . test ( codePart ) || codePart . endsWith ( ';' ) ) { ctx . inConditionBlock = false ; }
2026-10-11 02:31:12 +00:00
}
2026-10-12 19:43:46 +00:00
private checkSemicolons ( codePart : string , rawLine : string , lineIdx : number , ctx : LinterContext , originalTrimmedLine : string ) {
2026-10-11 02:31:12 +00:00
if ( ctx . isClosingString ) {
2026-10-12 19:43:46 +00:00
if ( ctx . stringNeedsSemicolon && ! originalTrimmedLine . endsWith ( ';' ) ) {
2026-10-11 02:31:12 +00:00
ctx . addDiag ( lineIdx , Math . max ( 0 , rawLine . length - 1 ) , rawLine . length , "Fehlendes Semikolon am Ende der Zuweisung." , vscode . DiagnosticSeverity . Error ) ;
}
} else {
2026-10-12 19:43:46 +00:00
// "then" und "do" dürfen am Anfang einer Zeile stehen, ohne ein Semikolon zu erfordern
const noSemicolonNeeded = /^(pattern|syncmapping|traverse|table|enumeration|configuration|definitions?|define|metadata|overview|constants|triggers|mapping|removal|body|end\s+|if\s+|else|elif\s+|for\s+|while\s+|then\b|do\b|\/\/|\/\*|\*)/ . test ( codePart ) ;
2026-10-11 02:31:12 +00:00
2026-10-12 19:43:46 +00:00
// "in" (für for-Schleifen) als gültigen Zeilenumbruch hinzugefügt
const continuesOnNextLine = /(,|and|or|not|\+|-|\*|\/|=|:=|matches|in)$/i . test ( codePart ) ;
// HIER GEFIXT: Wenn die Zeile auf 'then' oder 'do' endet, verlangt sie kein Semikolon!
const endsWithBlockOpener = /\b(then|do)$/ . test ( codePart ) ;
if ( ! noSemicolonNeeded && ! ctx . justOpenedString && ctx . parenStack . length === 0 && ctx . bracketStack . length === 0 && ctx . inBlock !== 'enumeration' && ! ctx . inConditionBlock && ! continuesOnNextLine && ! endsWithBlockOpener ) {
if ( codePart . length > 0 && ! codePart . endsWith ( ';' ) && ! codePart . endsWith ( '{' ) && ! codePart . endsWith ( ',' ) ) {
2026-10-11 02:31:12 +00:00
ctx . addDiag ( lineIdx , Math . max ( 0 , rawLine . length - 1 ) , rawLine . length , "Fehlendes Semikolon am Ende der Anweisung." , vscode . DiagnosticSeverity . Error ) ;
}
}
}
}
}