'use strict' ; import * as path from 'path' ; import * as cp from 'child_process' ; import * as vscode from 'vscode' ; import { LoggingService } from '../services/logging-service' ; import { FortranDocumentSelector , resolveVariables } from '../lib/tools' ; import * as fg from 'fast-glob' ; import { glob } from 'glob' ; export default class FortranLintingProvider { constructor ( private loggingService ) { } private diagnosticCollection : vscode . DiagnosticCollection ; private doModernFortranLint ( textDocument ) { const errorRegex = /^([a-zA-Z]:\\)*([^:]*):([0-9]+):([0-9]+):\s+(.*)\s+.*?\s+(Error|Warning|Fatal Error):\s(.*)$/gm ; if ( ! FortranDocumentSelector ( ) . some ( e => e . scheme === textDocument . uri . scheme ) || ! FortranDocumentSelector ( ) . some ( e => e . language === textDocument . languageId ) ) { return ; } let decoded = '' ; const diagnostics = [ ] ; const command = this . getGfortranPath ( ) ; const argList = this . constructArgumentList ( textDocument ) ; const filePath = path . parse ( textDocument . fileName ) . dir ; const env = process . env ; env . LC_ALL = 'C' ; if ( process . platform === 'win32' ) { if ( ! env . Path . includes ( path . dirname ( command ) ) ) { env . Path = `${ path . dirname ( command ) }${ path . delimiter }${ env . Path }` ; } } const childProcess = cp . spawn ( command , argList , { cwd : filePath , env : env , } ) ; if ( childProcess . pid ) { childProcess . stdout . on ( 'data' , ( data ) => { decoded += data ; } ) ; childProcess . stderr . on ( 'data' , data => { decoded += data ; } ) ; childProcess . stderr . on ( 'end' , ( ) => { let matchesArray ; while ( ( matchesArray = errorRegex . exec ( decoded ) ) !== null ) { const elements = matchesArray . slice ( 1 ) ; const startLine = parseInt ( elements [ 2 ] ) ; const startColumn = parseInt ( elements [ 3 ] ) ; const type = elements [ 5 ] ; const severity = type . toLowerCase ( ) === 'warning' ? vscode . DiagnosticSeverity . Warning : vscode . DiagnosticSeverity . Error ; const message = elements [ 6 ] ; const range = new vscode . Range ( new vscode . Position ( startLine - 1 , startColumn ) , new vscode . Position ( startLine - 1 , startColumn ) ) ; const diagnostic = new vscode . Diagnostic ( range , message , severity ) ; diagnostics . push ( diagnostic ) ; } this . diagnosticCollection . set ( textDocument . uri , diagnostics ) ; } ) ; childProcess . stdout . on ( 'close' , code => { console . log ( `child process exited with code ${ code }` ) ; } ) ; } else { childProcess . on ( 'error' , ( err ) => { if ( err . code === 'ENOENT' ) { vscode . window . showErrorMessage ( "gfortran can't found on path, update your settings with a proper path or disable the linter." ) ; } } ) ; } } private constructArgumentList ( textDocument ) { const args = [ '-fsyntax-only' , '-cpp' , '-fdiagnostics-show-option' , ... this . getLinterExtraArgs ( ) , this . getModOutputDir ( ) , ] ; const includePaths = this . getIncludePaths ( ) ; const extensionIndex = textDocument . fileName . lastIndexOf ( '.' ) ; const fileNameWithoutExtension = textDocument . fileName . substring ( 0 , extensionIndex ) ; const argList = [ ... args , ... this . getIncludeParams ( includePaths ) , textDocument . fileName , `-o ${ fileNameWithoutExtension }.mod` , ] ; return argList . map ( arg => arg . trim ( ) ) . filter ( arg => arg !== '' ) ; } private static commandId = 'fortran.lint.runCodeAction' ; public provideCodeActions ( document , range , context , token ) { return ; } private command : vscode . Disposable ; public activate ( subscriptions ) { this . diagnosticCollection = vscode . languages . createDiagnosticCollection ( 'Fortran' ) ; vscode . workspace . onDidOpenTextDocument ( this . doModernFortranLint , this , subscriptions ) ; vscode . workspace . onDidCloseTextDocument ( textDocument => { this . diagnosticCollection . delete ( textDocument . uri ) ; } , null , subscriptions ) ; vscode . workspace . onDidSaveTextDocument ( this . doModernFortranLint , this ) ; vscode . workspace . textDocuments . forEach ( this . doModernFortranLint , this ) ; } public dispose ( ) { this . diagnosticCollection . clear ( ) ; this . diagnosticCollection . dispose ( ) ; this . command . dispose ( ) ; } private getModOutputDir ( ) { const config = vscode . workspace . getConfiguration ( 'fortran' ) ; let modout = config . get ( 'linterModOutput' , '' ) ; if ( modout ) { modout = '-J' + resolveVariables ( modout ) ; this . loggingService . logInfo ( `Linter.moduleOutput` ) ; } return modout ; } private getIncludePaths ( ) { const config = vscode . workspace . getConfiguration ( 'fortran' ) ; const includePaths = config . get ( 'includePaths' , [ ] ) ; this . loggingService . logInfo ( `Linter.include:\n${ includePaths . join ( '\r\n' ) }` ) ; const resIncludePaths = includePaths . map ( e => resolveVariables ( e ) ) ; try { const globIncPaths = fg . sync ( resIncludePaths , { onlyDirectories : true , suppressErrors : false , } ) ; return globIncPaths ; } catch ( eacces ) { this . loggingService . logWarning ( `You lack read permissions for an include directory
          or more likely a glob match from the input 'includePaths' list. This can happen when
          using overly broad root level glob patters e.g. /usr/lib/** .
          No reason to worry. I will attempt to recover. 
          You should consider adjusting your 'includePaths' if linting performance is slow.` ) ; this . loggingService . logWarning ( `${ eacces . message }` ) ; try { const globIncPaths = [ ] ; for ( const i of resIncludePaths ) { globIncPaths . push ( ... glob . sync ( i + '/' , { strict : false } ) ) ; } return globIncPaths ; } catch ( error ) { this . loggingService . logError ( `Failed to recover: ${ error }` ) ; } } } private getGfortranPath ( ) { const config = vscode . workspace . getConfiguration ( 'fortran' ) ; const gfortranPath = config . get ( 'gfortranExecutable' , 'gfortran' ) ; this . loggingService . logInfo ( `using gfortran executable: ${ gfortranPath }` ) ; return gfortranPath ; } private getLinterExtraArgs ( ) { const config = vscode . workspace . getConfiguration ( 'fortran' ) ; const args = config . get ( 'linterExtraArgs' , [ '-Wall' ] ) . map ( e => resolveVariables ( e ) ) ; this . loggingService . logInfo ( `Linter.arguments:\n${ args . join ( '\r\n' ) }` ) ; return args . map ( e => resolveVariables ( e ) ) ; } private getIncludeParams = ( paths ) => { return paths . map ( path => `-I${ path }` ) ; } ; }