'use strict' ; import { Command } from 'commander' ; import { IPCMessageReader , IPCMessageWriter , createConnection , IConnection , TextDocuments , TextDocument , Diagnostic , DiagnosticSeverity , InitializeResult } from 'vscode-languageserver' ; import { URI } from 'vscode-uri' ; import { spawn } from "child_process" ; const program = new Command ( 'cfn-lsp' ) . allowUnknownOption ( ) . version ( require ( '../../package.json' ) . version ) . option ( '--stdio' , 'use stdio' ) . option ( '--node-ipc' , 'use node-ipc' ) . parse ( process . argv ) ; var connection ; if ( program . stdio ) { connection = createConnection ( process . stdin , process . stdout ) ; } else { connection = createConnection ( new IPCMessageReader ( process ) , new IPCMessageWriter ( process ) ) ; } let documents = new TextDocuments ( ) ; documents . listen ( connection ) ; let workspaceRoot ; connection . onInitialize ( ( params ) : InitializeResult => { workspaceRoot = params . rootPath ; return { capabilities : { textDocumentSync : documents . syncKind } } ; } ) ; documents . onDidSave ( ( event ) => { runLinter ( event . document ) ; } ) ; documents . onDidOpen ( ( event ) => { runLinter ( event . document ) ; } ) ; documents . onDidClose ( ( event ) => { connection . sendNotification ( 'cfn/fileclosed' , event . document . uri ) ; } ) ; connection . onNotification ( 'cfn/requestPreview' , ( uri ) => { connection . console . log ( 'preview requested: ' + uri ) ; isPreviewing [ uri ] = true ; runLinter ( documents . get ( uri ) ) ; } ) ; connection . onNotification ( 'cfn/previewClosed' , ( uri ) => { connection . console . log ( 'preview closed: ' + uri ) ; isPreviewing [ uri ] = false ; } ) ; interface Settings { cfnLint : CloudFormationLintSettings ; } interface CloudFormationLintSettings { path : string ; appendRules : Array < string > ; ignoreRules : Array < string > ; overrideSpecPath : string ; } let Path ; let AppendRules ; let IgnoreRules ; let OverrideSpecPath ; connection . onDidChangeConfiguration ( ( change ) => { connection . console . log ( 'Settings have been updated...' ) ; let settings = < Settings > change . settings ; connection . console . log ( 'Settings: ' + JSON . stringify ( settings ) ) ; Path = settings . cfnLint . path || 'cfn-lint' ; IgnoreRules = settings . cfnLint . ignoreRules ; OverrideSpecPath = settings . cfnLint . overrideSpecPath ; AppendRules = settings . cfnLint . appendRules ; documents . all ( ) . forEach ( runLinter ) ; } ) ; let isRunningLinterOn = { } ; let isPreviewing = { } ; function convertSeverity ( mistakeType ) { switch ( mistakeType ) { case "Warning" : return DiagnosticSeverity . Warning ; case "Informational" : return DiagnosticSeverity . Information ; case "Hint" : return DiagnosticSeverity . Hint ; } return DiagnosticSeverity . Error ; } function isCloudFormation ( template , filename ) { if ( /"?AWSTemplateFormatVersion"?\s*/ . exec ( template ) ) { connection . console . log ( "Determined this file is a CloudFormation Template. " + filename + ". Found the string AWSTemplateFormatVersion" ) ; return true ; } if ( /\n?"?Resources"?\s*:/ . exec ( template ) ) { if ( /"?Type"?\s*:\s*"?'?[a-zA-Z0-9]{2,64}::[a-zA-Z0-9]{2,64}/ . exec ( template ) ) { if ( ! ( /\nresources:/ . exec ( template ) && /\nprovider:/ . exec ( template ) ) ) { connection . console . log ( "Determined this file is a CloudFormation Template. " + filename + ". Found 'Resources' and 'Type: [a-zA-Z0-9]{2,64}::[a-zA-Z0-9]{2,64}'" ) ; return true ; } } } return false ; } function runLinter ( document ) { let uri = document . uri ; if ( isRunningLinterOn [ uri ] ) { connection . sendNotification ( 'cfn/busy' ) ; return ; } let file_to_lint = URI . parse ( uri ) . fsPath ; let is_cfn = isCloudFormation ( document . getText ( ) , uri . toString ( ) ) ; connection . sendNotification ( 'cfn/isPreviewable' , is_cfn ) ; let build_graph = isPreviewing [ uri ] ; if ( is_cfn ) { let args = [ '--format' , 'json' ] ; if ( build_graph ) { args . push ( '--build-graph' ) ; } if ( IgnoreRules . length > 0 ) { for ( var ignoreRule of IgnoreRules ) { args . push ( '--ignore-checks' ) ; args . push ( ignoreRule ) ; } } if ( AppendRules . length > 0 ) { for ( var appendRule of AppendRules ) { args . push ( '--append-rules' ) ; args . push ( appendRule ) ; } } if ( OverrideSpecPath !== "" ) { args . push ( '--override-spec' , OverrideSpecPath ) ; } args . push ( '--' , `"${ file_to_lint }"` ) ; connection . console . log ( `Running... ${ Path } ${ args }` ) ; isRunningLinterOn [ uri ] = true ; let child = spawn ( Path , args , { cwd : workspaceRoot , shell : true } ) ; let diagnostics = [ ] ; let filename = uri . toString ( ) ; let start = 0 ; let end = Number . MAX_VALUE ; child . on ( 'error' , function ( err ) { let errorMessage = `Unable to start cfn-lint (${ err }). Is cfn-lint installed correctly?` ; connection . console . log ( errorMessage ) ; let lineNumber = 0 ; let diagnostic = { range : { start : { line : lineNumber , character : start } , end : { line : lineNumber , character : end } } , severity : DiagnosticSeverity . Error , message : '[cfn-lint] ' + errorMessage } ; diagnostics . push ( diagnostic ) ; return ; } ) ; child . stderr . on ( "data" , ( data ) => { let err = data . toString ( ) ; connection . console . log ( err ) ; let lineNumber = 0 ; let diagnostic = { range : { start : { line : lineNumber , character : start } , end : { line : lineNumber , character : end } } , severity : DiagnosticSeverity . Warning , message : '[cfn-lint] ' + err + '\nGo to https://github.com/aws-cloudformation/cfn-python-lint/#install for more help' } ; diagnostics . push ( diagnostic ) ; return ; } ) ; let stdout = "" ; child . stdout . on ( "data" , ( data ) => { stdout = stdout . concat ( data . toString ( ) ) ; } ) ; child . on ( 'exit' , function ( code , signal ) { connection . console . log ( 'child process exited with ' + `code ${ code } and signal ${ signal }` ) ; let tmp = stdout . toString ( ) ; let obj ; try { obj = JSON . parse ( tmp ) ; } catch ( err ) { let lineNumber = 0 ; let diagnostic = { range : { start : { line : lineNumber , character : start } , end : { line : lineNumber , character : end } } , severity : DiagnosticSeverity . Warning , message : '[cfn-lint] ' + err + '\nGo to https://github.com/aws-cloudformation/cfn-python-lint/#install for more help' } ; diagnostics . push ( diagnostic ) ; return ; } for ( let element of obj ) { let lineNumber = ( Number . parseInt ( element . Location . Start . LineNumber ) - 1 ) ; let columnNumber = ( Number . parseInt ( element . Location . Start . ColumnNumber ) - 1 ) ; let lineNumberEnd = ( Number . parseInt ( element . Location . End . LineNumber ) - 1 ) ; let columnNumberEnd = ( Number . parseInt ( element . Location . End . ColumnNumber ) - 1 ) ; let diagnostic = { range : { start : { line : lineNumber , character : columnNumber } , end : { line : lineNumberEnd , character : columnNumberEnd } } , severity : convertSeverity ( element . Level ) , message : '[cfn-lint] ' + element . Rule . Id + ': ' + element . Message } ; diagnostics . push ( diagnostic ) ; } ; } ) ; child . on ( "close" , ( ) => { connection . sendDiagnostics ( { uri : filename , diagnostics } ) ; isRunningLinterOn [ uri ] = false ; if ( build_graph ) { connection . console . log ( 'preview file is available' ) ; connection . sendNotification ( 'cfn/previewIsAvailable' , uri ) ; } } ) ; } else { connection . console . log ( "Don't believe this is a CloudFormation template. " + uri . toString ( ) + ". If it is please add AWSTemplateFormatVersion: '2010-09-09' (YAML) or " + " \"AWSTemplateFormatVersion\": \"2010-09-09\" (JSON) into the root level of the document." ) ; } } connection . listen ( ) ;