import { DiagnosticSeverity , Diagnostic , Range , TextDocument } from "vscode-languageserver" ; import { SystemVerilogIndexer } from "./svindexer" ; import { ConnectionLogger , fsWriteFileSync , fsUnlinkSync , getTmpDirSync } from "./genutils" ; import * as child from 'child_process' ; const path = require ( 'path' ) ; type TimerType = ReturnType < typeof setTimeout > ; export class VerilatorDiagnostics { private static readonly _whitelistedMessages : RegExp [ ] = [ /Unsupported: Interfaced port on top level module/i ] ; private _indexer : SystemVerilogIndexer ; private _command : string = "" ; private _defines : string [ ] = [ ] ; private _optionsFile : string = "" ; private _alreadyRunning : Map < string , [ child . ChildProcess , [ Boolean ] ] > = new Map ( ) ; private _fileWaiting : Map < string , [ TimerType , any ] > = new Map ( ) ; private _tmpDir ; private _freeTmpFileNums : number [ ] = [ ] ; private _totalTmpFileNums : number = 0 ; constructor ( indexer ) { this . _indexer = indexer ; this . _tmpDir = getTmpDirSync ( ) ; } public setCommand ( cmd ) { this . _command = cmd ; } public setOptionsFile ( file ) { this . _optionsFile = file ; } public setDefines ( defines ) { this . _defines = defines || [ ] ; } private _getFreeTmpFileNum ( ) { if ( this . _freeTmpFileNums . length <= 0 ) { this . _freeTmpFileNums . push ( this . _totalTmpFileNums ++ ) ; } return this . _freeTmpFileNums . shift ( ) ; } private _lintImmediate ( file , text ? ) { let _kill = ( ) => { let [ proc , statusRef ] = this . _alreadyRunning . get ( file ) ; statusRef [ 0 ] = false ; proc . kill ( ) ; } ; if ( this . _alreadyRunning . has ( file ) ) { _kill ( ) ; } return new Promise ( ( resolve ) => { let actFile = text == undefined ? file : path . join ( this . _tmpDir . name , "sources" , file ) ; let optionsFile = this . _optionsFile ; let vcTmpFileNum ; if ( text != undefined ) { fsWriteFileSync ( actFile , text ) ; if ( this . _alreadyRunning . has ( file ) ) { _kill ( ) ; } if ( this . _indexer . fileHasPkg ( file ) ) { let vcFileContent = this . _indexer . getOptionsFileContent ( ) . map ( line => { return ( line == file ) ? actFile : line ; } ) . join ( '\n' ) ; vcTmpFileNum = this . _getFreeTmpFileNum ( ) ; let tmpVcFile = path . join ( this . _tmpDir . name , "vcfiles" , `lint${ vcTmpFileNum }.vc` ) ; fsWriteFileSync ( tmpVcFile , vcFileContent ) ; optionsFile = tmpVcFile ; if ( this . _alreadyRunning . has ( file ) ) { _kill ( ) ; } } } let definesArg = this . _defines . length > 0 ? this . _defines . map ( d => ` +define+${ d }` ) . join ( '' ) : "" ; let optionsFileArg = optionsFile ? ' -f ' + optionsFile : "" ; let actFileArg = ( this . _indexer . fileHasPkg ( file ) ) ? "" : " " + actFile ; let command = this . _command + definesArg + optionsFileArg + actFileArg ; let statusRef = [ true ] ; this . _alreadyRunning . set ( file , [ child . exec ( command , ( error , stdout , stderr ) => { if ( optionsFile != this . _optionsFile ) { this . _freeTmpFileNums . push ( vcTmpFileNum ) ; } if ( statusRef [ 0 ] ) { this . _alreadyRunning . delete ( file ) ; resolve ( this . _parseDiagnostics ( error , stdout , stderr , actFile ) ) ; } else { resolve ( [ ] ) ; } } ) , statusRef ] ) ; } ) ; } public lint ( file , text ? ) { try { if ( text == undefined ) { return this . _lintImmediate ( file , text ) . catch ( error => { ConnectionLogger . error ( error ) ; return [ ] ; } ) ; } else { if ( this . _fileWaiting . has ( file ) ) { let [ waitTimer , resolver ] = this . _fileWaiting . get ( file ) ; clearTimeout ( waitTimer ) ; resolver ( false ) ; } return new Promise ( resolve => { this . _fileWaiting . set ( file , [ setTimeout ( resolve , 1000 , true ) , resolve ] ) ; } ) . then ( ( success ) => { if ( ! ! success ) { this . _fileWaiting . delete ( file ) ; return this . _lintImmediate ( file , text ) ; } return [ ] ; } ) . catch ( error => { ConnectionLogger . error ( error ) ; return [ ] ; } ) ; } } catch ( error ) { ConnectionLogger . error ( error ) ; return Promise . resolve ( [ ] ) ; } } private _parseDiagnostics ( error , stdout , stderr , file ) { let diagnostics = [ ] ; let lines = stderr . split ( /\r?\n/g ) ; let regex = new RegExp ( String . raw `%(Error|Warning)(-[A-Z0-9_]+)?: (` + file . replace ( /[-[\]{}()*+?.,\\^$|#\s]/g , '\\$&' ) + String . raw `):(\d+):(?:(\d+):)? (.*)` , 'i' ) ; lines . forEach ( ( line , i ) => { let terms = line . match ( regex ) ; if ( terms != null ) { let severity = this . _getSeverity ( terms [ 1 ] ) ; let message = "" ; let lineNum = parseInt ( terms [ 4 ] ) - 1 ; let colNum = 0 ; if ( terms [ 5 ] ) { colNum = parseInt ( terms [ 5 ] ) - 1 ; } message = terms [ 6 ] ; for ( let whitelistedMessage of VerilatorDiagnostics . _whitelistedMessages ) { if ( message . search ( whitelistedMessage ) >= 0 ) { return ; } } if ( ( lineNum != NaN ) && ( colNum != NaN ) ) { diagnostics . push ( { severity : severity , range : Range . create ( lineNum , colNum , lineNum , Number . MAX_VALUE ) , message : message , code : 'verilator' , source : 'verilator' } ) ; } } } ) ; return diagnostics ; } private _getSeverity ( severityString ) { let result = DiagnosticSeverity . Information ; if ( severityString . startsWith ( 'Error' ) ) { result = DiagnosticSeverity . Error ; } else if ( severityString . startsWith ( 'Warning' ) ) { result = DiagnosticSeverity . Warning ; } return result ; } public cleanupTmpFiles ( ) { this . _tmpDir . removeCallback ( ) ; } }