'use strict' ; import * as ChildProcess from 'child_process' ; import * as FS from 'fs' ; import { DateTime } from 'luxon' ; import * as Net from 'net' ; import * as Path from 'path' ; import * as VSCode from 'vscode' ; import { LanguageClientOptions , StreamInfo } from 'vscode-languageclient/node' ; import { SonarLintExtendedLanguageClient } from './client' ; import { Commands } from './commands' ; import { GitExtension } from './git' ; import { hideSecurityHotspot , HotspotsCodeActionProvider , hotspotsCollection , showHotspotDescription , showSecurityHotspot } from './hotspots' ; import { getJavaConfig , installClasspathListener } from './java' ; import { LocationTreeItem , navigateToLocation , SecondaryLocationsTree } from './locations' ; import * as protocol from './protocol' ; import { installManagedJre , JAVA_HOME_CONFIG , RequirementsData , resolveRequirements } from './requirements' ; import { computeRuleDescPanelContent } from './rulepanel' ; import { AllRulesTreeDataProvider , ConfigLevel , Rule , RuleNode } from './rules' ; import { initScm } from './scm' ; import { code2ProtocolConverter , protocol2CodeConverter } from './uri' ; import * as util from './util' ; declare let v8debug ; const DEBUG = typeof v8debug === 'object' || util . startedInDebugMode ( process ) ; let currentConfig ; const FIRST_SECRET_ISSUE_DETECTED_KEY = 'FIRST_SECRET_ISSUE_DETECTED_KEY' ; const DOCUMENT_SELECTOR = [ { scheme : 'file' , pattern : '**/*' } ] ; let sonarlintOutput ; let ruleDescriptionPanel ; let secondaryLocationsTree ; let issueLocationsView ; let languageClient ; export function logToSonarLintOutput ( message ) { if ( sonarlintOutput ) { sonarlintOutput . appendLine ( message ) ; } } function runJavaServer ( context ) { return resolveRequirements ( context ) . catch ( error => { VSCode . window . showErrorMessage ( error . message , error . label ) . then ( selection => { if ( error . label && error . label === selection && error . command ) { VSCode . commands . executeCommand ( error . command , error . commandParam ) ; } } ) ; throw error ; } ) . then ( requirements => { return new Promise < StreamInfo > ( ( resolve , reject ) => { const server = Net . createServer ( socket => { if ( isVerboseEnabled ( ) ) { logToSonarLintOutput ( `Child process connected on port ${ ( server . address ( ) as Net . AddressInfo ) . port }` ) ; } resolve ( { reader : socket , writer : socket } ) ; } ) ; server . listen ( 0 , ( ) => { const port = ( server . address ( ) as Net . AddressInfo ) . port ; const { command , args } = languageServerCommand ( context , requirements , port ) ; if ( isVerboseEnabled ( ) ) { logToSonarLintOutput ( `Executing ${ command } ${ args . join ( ' ' ) }` ) ; } const process = ChildProcess . spawn ( command , args ) ; process . stdout . on ( 'data' , function ( data ) { logWithPrefix ( data , '[stdout]' ) ; } ) ; process . stderr . on ( 'data' , function ( data ) { logWithPrefix ( data , '[stderr]' ) ; } ) ; } ) ; } ) ; } ) ; } function logWithPrefix ( data , prefix ) { if ( isVerboseEnabled ( ) ) { const lines = data . toString ( ) . split ( /\r\n|\r|\n/ ) ; lines . forEach ( ( l ) => { if ( l . length > 0 ) { logToSonarLintOutput ( `${ prefix } ${ l }` ) ; } } ) ; } } function isVerboseEnabled ( ) { return currentConfig . get ( 'output.showVerboseLogs' , false ) ; } function languageServerCommand ( context , requirements , port ) { const serverJar = Path . resolve ( context . extensionPath , 'server' , 'sonarlint-ls.jar' ) ; const javaExecutablePath = Path . resolve ( requirements . javaHome + '/bin/java' ) ; const params = [ ] ; if ( DEBUG ) { params . push ( '-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000' ) ; params . push ( '-Dsonarlint.telemetry.disabled=true' ) ; } const vmargs = getSonarLintConfiguration ( ) . get ( 'ls.vmargs' , '' ) ; parseVMargs ( params , vmargs ) ; params . push ( '-jar' , serverJar , `${ port }` ) ; params . push ( '-analyzers' ) ; params . push ( toUrl ( Path . resolve ( context . extensionPath , 'analyzers' , 'sonarjava.jar' ) ) ) ; params . push ( toUrl ( Path . resolve ( context . extensionPath , 'analyzers' , 'sonarjs.jar' ) ) ) ; params . push ( toUrl ( Path . resolve ( context . extensionPath , 'analyzers' , 'sonarphp.jar' ) ) ) ; params . push ( toUrl ( Path . resolve ( context . extensionPath , 'analyzers' , 'sonarpython.jar' ) ) ) ; params . push ( toUrl ( Path . resolve ( context . extensionPath , 'analyzers' , 'sonarhtml.jar' ) ) ) ; const secrets = Path . resolve ( context . extensionPath , 'analyzers' , 'sonarsecrets.jar' ) ; if ( FS . existsSync ( secrets ) ) { params . push ( '-extraAnalyzers' ) ; params . push ( toUrl ( Path . resolve ( context . extensionPath , 'analyzers' , 'sonarsecrets.jar' ) ) ) ; } return { command : javaExecutablePath , args : params } ; } export function toUrl ( filePath ) { let pathName = Path . resolve ( filePath ) . replace ( /\\/g , '/' ) ; if ( pathName [ 0 ] !== '/' ) { pathName = '/' + pathName ; } return encodeURI ( 'file://' + pathName ) ; } function resolveInAnyWorkspaceFolder ( tsdkPathSetting ) { if ( Path . isAbsolute ( tsdkPathSetting ) ) { return FS . existsSync ( tsdkPathSetting ) ? tsdkPathSetting : undefined ; } for ( const folder of VSCode . workspace . workspaceFolders || [ ] ) { const configuredTsPath = Path . join ( folder . uri . fsPath , tsdkPathSetting ) ; if ( FS . existsSync ( configuredTsPath ) ) { return configuredTsPath ; } } return undefined ; } function findTypeScriptLocation ( ) { const tsExt = VSCode . extensions . getExtension ( 'vscode.typescript-language-features' ) ; if ( tsExt ) { const bundledTypeScriptPath = Path . resolve ( tsExt . extensionPath , '..' , 'node_modules' , 'typescript' , 'lib' ) ; if ( ! FS . existsSync ( bundledTypeScriptPath ) ) { logToSonarLintOutput ( `Unable to locate bundled TypeScript module in "${ bundledTypeScriptPath }". Please report this error to SonarLint project.` ) ; } const tsdkPathSetting = VSCode . workspace . getConfiguration ( 'typescript' ) . get ( 'tsdk' ) ; if ( tsdkPathSetting ) { const configuredTsPath = resolveInAnyWorkspaceFolder ( tsdkPathSetting ) ; if ( configuredTsPath !== undefined ) { return configuredTsPath ; } logToSonarLintOutput ( `Unable to locate TypeScript module in "${ configuredTsPath }". Falling back to the VSCode's one at "${ bundledTypeScriptPath }"` ) ; } return bundledTypeScriptPath ; } else { logToSonarLintOutput ( 'Unable to locate TypeScript extension. TypeScript support in SonarLint might not work.' ) ; return undefined ; } } function toggleRule ( level ) { return ( ruleKey ) => { const configuration = getSonarLintConfiguration ( ) ; const rules = configuration . get ( 'rules' ) || { } ; if ( typeof ruleKey === 'string' ) { rules [ ruleKey ] = { level } ; } else { const { key , activeByDefault } = ruleKey . rule ; if ( ( level === 'on' && ! activeByDefault ) || ( level === 'off' && activeByDefault ) ) { rules [ key ] = { level } ; } else { rules [ key ] = undefined ; } } return configuration . update ( 'rules' , rules , VSCode . ConfigurationTarget . Global ) ; } ; } export function activate ( context ) { currentConfig = getSonarLintConfiguration ( ) ; util . setExtensionContext ( context ) ; sonarlintOutput = VSCode . window . createOutputChannel ( 'SonarLint' ) ; context . subscriptions . push ( sonarlintOutput ) ; const serverOptions = ( ) => runJavaServer ( context ) ; const tsPath = findTypeScriptLocation ( ) ; const pythonWatcher = VSCode . workspace . createFileSystemWatcher ( '**/*.py' ) ; context . subscriptions . push ( pythonWatcher ) ; const clientOptions = { documentSelector : DOCUMENT_SELECTOR , synchronize : { configurationSection : 'sonarlint' , fileEvents : pythonWatcher } , uriConverters : { code2Protocol : code2ProtocolConverter , protocol2Code : protocol2CodeConverter } , diagnosticCollectionName : 'sonarlint' , initializationOptions : ( ) => { return { productKey : 'vscode' , telemetryStorage : Path . resolve ( context . extensionPath , '..' , 'sonarlint_usage' ) , productName : 'SonarLint VSCode' , productVersion : util . packageJson . version , workspaceName : VSCode . workspace . name , typeScriptLocation : tsPath ? Path . dirname ( Path . dirname ( tsPath ) ) : undefined , firstSecretDetected : context . globalState . get ( FIRST_SECRET_ISSUE_DETECTED_KEY ) === 'true' ? 'true' : 'false' , additionalAttributes : { vscode : { remoteName : VSCode . env . remoteName , uiKind : VSCode . UIKind [ VSCode . env . uiKind ] } } } ; } , outputChannel : sonarlintOutput , revealOutputChannelOn : 4 } ; languageClient = new SonarLintExtendedLanguageClient ( 'sonarlint' , 'SonarLint Language Server' , serverOptions , clientOptions ) ; languageClient . onReady ( ) . then ( ( ) => installCustomRequestHandlers ( context ) ) ; languageClient . onReady ( ) . then ( ( ) => { const scm = initScm ( languageClient ) ; context . subscriptions . push ( scm ) ; context . subscriptions . push ( languageClient . onRequest ( protocol . GetBranchNameForFolderRequest . type , folderUri => { return scm . getBranchForFolder ( VSCode . Uri . parse ( folderUri ) ) ; } ) ) ; } ) ; const allRulesTreeDataProvider = new AllRulesTreeDataProvider ( ( ) => languageClient . onReady ( ) . then ( ( ) => languageClient . listAllRules ( ) ) ) ; const allRulesView = VSCode . window . createTreeView ( 'SonarLint.AllRules' , { treeDataProvider : allRulesTreeDataProvider } ) ; context . subscriptions . push ( allRulesView ) ; secondaryLocationsTree = new SecondaryLocationsTree ( ) ; issueLocationsView = VSCode . window . createTreeView ( 'SonarLint.IssueLocations' , { treeDataProvider : secondaryLocationsTree } ) ; context . subscriptions . push ( issueLocationsView ) ; context . subscriptions . push ( VSCode . commands . registerCommand ( Commands . SHOW_ALL_LOCATIONS , showAllLocations ) ) ; context . subscriptions . push ( VSCode . commands . registerCommand ( Commands . CLEAR_LOCATIONS , clearLocations ) ) ; context . subscriptions . push ( VSCode . commands . registerCommand ( Commands . NAVIGATE_TO_LOCATION , navigateToLocation ) ) ; context . subscriptions . push ( VSCode . commands . registerCommand ( Commands . DEACTIVATE_RULE , toggleRule ( 'off' ) ) ) ; context . subscriptions . push ( VSCode . commands . registerCommand ( Commands . ACTIVATE_RULE , toggleRule ( 'on' ) ) ) ; context . subscriptions . push ( VSCode . commands . registerCommand ( Commands . SHOW_ALL_RULES , ( ) => allRulesTreeDataProvider . filter ( null ) ) ) ; context . subscriptions . push ( VSCode . commands . registerCommand ( Commands . SHOW_ACTIVE_RULES , ( ) => allRulesTreeDataProvider . filter ( 'on' ) ) ) ; context . subscriptions . push ( VSCode . commands . registerCommand ( Commands . SHOW_INACTIVE_RULES , ( ) => allRulesTreeDataProvider . filter ( 'off' ) ) ) ; context . subscriptions . push ( VSCode . commands . registerCommand ( Commands . FIND_RULE_BY_KEY , ( ) => { VSCode . window . showInputBox ( { prompt : 'Rule Key' , validateInput : value => allRulesTreeDataProvider . checkRuleExists ( value ) } ) . then ( key => { VSCode . commands . executeCommand ( Commands . SHOW_ALL_RULES ) . then ( ( ) => allRulesView . reveal ( new RuleNode ( { key : key . toUpperCase ( ) } as Rule ) , { focus : true , expand : true } ) ) ; } ) ; } ) ) ; context . subscriptions . push ( VSCode . commands . registerCommand ( Commands . SHOW_SONARLINT_OUTPUT , ( ) => { sonarlintOutput . show ( ) ; } ) ) ; context . subscriptions . push ( VSCode . commands . registerCommand ( Commands . INSTALL_MANAGED_JRE , installManagedJre ) ) ; context . subscriptions . push ( hotspotsCollection ) ; context . subscriptions . push ( VSCode . languages . registerCodeActionsProvider ( { scheme : 'file' } , new HotspotsCodeActionProvider ( ) , { providedCodeActionKinds : [ VSCode . CodeActionKind . Empty ] } ) ) ; context . subscriptions . push ( VSCode . commands . registerCommand ( Commands . HIDE_HOTSPOT , hideSecurityHotspot ) ) ; context . subscriptions . push ( VSCode . commands . registerCommand ( Commands . SHOW_HOTSPOT_DESCRIPTION , showHotspotDescription ) ) ; VSCode . workspace . onDidChangeConfiguration ( async ( event ) => { if ( event . affectsConfiguration ( 'sonarlint.rules' ) ) { allRulesTreeDataProvider . refresh ( ) ; } } ) ; languageClient . start ( ) ; context . subscriptions . push ( onConfigurationChange ( ) ) ; context . subscriptions . push ( VSCode . extensions . onDidChange ( ( ) => { installClasspathListener ( languageClient ) ; } ) ) ; installClasspathListener ( languageClient ) ; } async function showNotificationForFirstSecretsIssue ( context ) { const showProblemsViewActionTitle = 'Show Problems View' ; VSCode . window . showWarningMessage ( 'SonarLint detected some secrets in one of the open files.\n' + 'We strongly advise you to review those secrets and ensure they are not committed into repositories. ' + 'Please refer to the Problems view for more information.' , showProblemsViewActionTitle ) . then ( action => { if ( action === showProblemsViewActionTitle ) { VSCode . commands . executeCommand ( 'workbench.panel.markers.view.focus' ) ; } } ) ; context . globalState . update ( FIRST_SECRET_ISSUE_DETECTED_KEY , 'true' ) ; } function installCustomRequestHandlers ( context ) { languageClient . onRequest ( protocol . ShowRuleDescriptionRequest . type , params => { if ( ! ruleDescriptionPanel ) { ruleDescriptionPanel = VSCode . window . createWebviewPanel ( 'sonarlint.RuleDesc' , 'SonarLint Rule Description' , VSCode . ViewColumn . Two , { enableScripts : false } ) ; ruleDescriptionPanel . onDidDispose ( ( ) => { ruleDescriptionPanel = undefined ; } , null , context . subscriptions ) ; } const ruleDescPanelContent = computeRuleDescPanelContent ( context , ruleDescriptionPanel . webview , params ) ; ruleDescriptionPanel . webview . html = ruleDescPanelContent ; ruleDescriptionPanel . iconPath = { light : util . resolveExtensionFile ( 'images/sonarlint.svg' ) , dark : util . resolveExtensionFile ( 'images/sonarlint.svg' ) } ; ruleDescriptionPanel . reveal ( ) ; } ) ; languageClient . onRequest ( protocol . GetJavaConfigRequest . type , fileUri => getJavaConfig ( languageClient , fileUri ) ) ; languageClient . onRequest ( protocol . ScmCheckRequest . type , fileUri => isIgnoredByScm ( fileUri ) ) ; languageClient . onRequest ( protocol . ShowNotificationForFirstSecretsIssueRequest . type , ( ) => showNotificationForFirstSecretsIssue ( context ) ) ; languageClient . onRequest ( protocol . ShowSonarLintOutput . type , ( ) => VSCode . commands . executeCommand ( Commands . SHOW_SONARLINT_OUTPUT ) ) ; languageClient . onRequest ( protocol . OpenJavaHomeSettings . type , ( ) => VSCode . commands . executeCommand ( Commands . OPEN_SETTINGS , JAVA_HOME_CONFIG ) ) ; languageClient . onRequest ( protocol . OpenPathToNodeSettings . type , ( ) => VSCode . commands . executeCommand ( Commands . OPEN_SETTINGS , 'sonarlint.pathToNodeExecutable' ) ) ; languageClient . onRequest ( protocol . BrowseTo . type , browseTo => VSCode . commands . executeCommand ( Commands . OPEN_BROWSER , VSCode . Uri . parse ( browseTo ) ) ) ; languageClient . onRequest ( protocol . OpenConnectionSettings . type , isSonarCloud => { const targetSection = `sonarlint.connectedMode.connections.${ isSonarCloud ? 'sonarcloud' : 'sonarqube' }` ; return VSCode . commands . executeCommand ( Commands . OPEN_SETTINGS , targetSection ) ; } ) ; languageClient . onRequest ( protocol . ShowHotspotRequest . type , showSecurityHotspot ) ; languageClient . onRequest ( protocol . ShowTaintVulnerabilityRequest . type , showAllLocations ) ; } enum GitReturnCode { E_OK = 0 , E_FAIL = 1 , E_INVALID = 128 } function isNeitherOkNorFail ( code ? ) { return [ GitReturnCode . E_OK , GitReturnCode . E_FAIL ] . indexOf ( code ) < 0 ; } async function isIgnored ( workspaceFolderPath , gitCommand ) { const { sout , serr } = await new Promise < { sout : string ; serr : string ; } > ( ( resolve , reject ) => { ChildProcess . exec ( gitCommand , { cwd : workspaceFolderPath } , ( error , stdout , stderr ) => { if ( error && isNeitherOkNorFail ( error . code ) ) { if ( isVerboseEnabled ( ) ) { logToSonarLintOutput ( `Error on git command "${ gitCommand }": ${ error }` ) ; } reject ( error ) ; return ; } resolve ( { sout : stdout , serr : stderr } ) ; } ) ; } ) ; if ( serr ) { return Promise . resolve ( false ) ; } return Promise . resolve ( sout . length > 0 ) ; } async function isIgnoredByScm ( fileUri ) { return performIsIgnoredCheck ( fileUri , isIgnored ) ; } export async function performIsIgnoredCheck ( fileUri , scmCheck ) { const parsedFileUri = VSCode . Uri . parse ( fileUri ) ; const workspaceFolder = VSCode . workspace . getWorkspaceFolder ( parsedFileUri ) ; if ( workspaceFolder == null ) { return Promise . resolve ( false ) ; } const relativeFileUri = VSCode . workspace . asRelativePath ( parsedFileUri , false ) ; const gitExtension = VSCode . extensions . getExtension < GitExtension > ( 'vscode.git' ) . exports ; if ( gitExtension == null ) { return Promise . resolve ( false ) ; } try { const gitPath = gitExtension . getAPI ( 1 ) . git . path ; const command = `"${ gitPath }" check-ignore ${ relativeFileUri }` ; const fileIgnoredForFolder = await scmCheck ( workspaceFolder . uri . fsPath , command ) ; return Promise . resolve ( fileIgnoredForFolder ) ; } catch ( e ) { return Promise . resolve ( false ) ; } } async function showAllLocations ( issue ) { await secondaryLocationsTree . showAllLocations ( issue ) ; if ( issue . creationDate ) { const createdAgo = DateTime . fromISO ( issue . creationDate ) . toLocaleString ( DateTime . DATETIME_MED ) ; issueLocationsView . message = `Analyzed ${ createdAgo } on '${ issue . connectionId }'` ; } else { issueLocationsView . message = null ; } issueLocationsView . reveal ( secondaryLocationsTree . getChildren ( null ) [ 0 ] ) ; } function clearLocations ( ) { secondaryLocationsTree . hideLocations ( ) ; issueLocationsView . message = null ; } function onConfigurationChange ( ) { return VSCode . workspace . onDidChangeConfiguration ( event => { if ( ! event . affectsConfiguration ( 'sonarlint' ) ) { return ; } const newConfig = getSonarLintConfiguration ( ) ; const sonarLintLsConfigChanged = hasSonarLintLsConfigChanged ( currentConfig , newConfig ) || hasNodeJsConfigChanged ( currentConfig , newConfig ) ; if ( sonarLintLsConfigChanged ) { const msg = 'SonarLint Language Server configuration changed, please restart VS Code.' ; const action = 'Restart Now' ; const restartId = 'workbench.action.reloadWindow' ; currentConfig = newConfig ; VSCode . window . showWarningMessage ( msg , action ) . then ( selection => { if ( action === selection ) { VSCode . commands . executeCommand ( restartId ) ; } } ) ; } } ) ; } function hasSonarLintLsConfigChanged ( oldConfig , newConfig ) { return ! configKeyEquals ( 'ls.javaHome' , oldConfig , newConfig ) || ! configKeyEquals ( 'ls.vmargs' , oldConfig , newConfig ) ; } function hasNodeJsConfigChanged ( oldConfig , newConfig ) { return ! configKeyEquals ( 'pathToNodeExecutable' , oldConfig , newConfig ) ; } function configKeyEquals ( key , oldConfig , newConfig ) { return oldConfig . get ( key ) === newConfig . get ( key ) ; } function configKeyDeepEquals ( key , oldConfig , newConfig ) { return JSON . stringify ( oldConfig . get ( key ) ) === JSON . stringify ( newConfig . get ( key ) ) ; } export function parseVMargs ( params , vmargsLine ) { if ( ! vmargsLine ) { return ; } const vmargs = vmargsLine . match ( /(?:[^\s"]+|"[^"]*")+/g ) ; if ( vmargs === null ) { return ; } vmargs . forEach ( arg => { arg = arg . replace ( /(\\)?"/g , function ( $0 , $1 ) { return $1 ? $0 : '' ; } ) ; arg = arg . replace ( /(\\)"/g , '"' ) ; if ( params . indexOf ( arg ) < 0 ) { params . push ( arg ) ; } } ) ; } function getSonarLintConfiguration ( ) { return VSCode . workspace . getConfiguration ( 'sonarlint' ) ; } export function deactivate ( ) { if ( ! languageClient ) { return undefined ; } return languageClient . stop ( ) ; }