@@ -26,6 +26,7 @@ let previewedDocument: vscode.TextDocument | undefined;
2626let cppcheckProgressIndicator : vscode . StatusBarItem ;
2727let severityOption : vscode . StatusBarItem ;
2828let hiddenTypesOption : vscode . StatusBarItem ;
29+ let fullAnalysisStatusBarItem : vscode . StatusBarItem ;
2930let checksRunning = false ;
3031let usesPremiumCppcheck = false ;
3132
@@ -60,6 +61,26 @@ const pathVariableArgs = [
6061 '--rule-file' ,
6162] ;
6263
64+ async function processArguments ( args : string ) {
65+ // If user enter arguments as array we parse them into space separated string format
66+ if ( args . startsWith ( "[" ) && args . endsWith ( "]" ) ) {
67+ args = args . replaceAll ( "[" , "" ) . replaceAll ( "]" , "" ) . replaceAll ( "," , " " ) ;
68+ }
69+
70+ var processedArgs = '' ;
71+ // If argument field contains command to run script we do so here
72+ if ( args . includes ( '@(' ) ) {
73+ const scriptCommand = args . split ( "@(" ) [ 1 ] . split ( ")" ) [ 0 ] ;
74+ const scriptOutput = await runCommand ( scriptCommand ) ;
75+ // We expect that the script output that is to be used as arguments will be wrapped with ${}
76+ const scriptOutputTrimmed = scriptOutput . split ( "@(" ) [ 1 ] . split ( ")" ) [ 0 ] ;
77+ processedArgs = args . split ( "@(" ) [ 0 ] + scriptOutputTrimmed + args . split ( ")" ) ?. [ 1 ] ;
78+ } else {
79+ processedArgs = args ;
80+ }
81+ return processedArgs ;
82+ }
83+
6384function parseSeverity ( str : string ) : vscode . DiagnosticSeverity {
6485 const lower = str . toLowerCase ( ) ;
6586 if ( lower . includes ( "error" ) ) {
@@ -111,9 +132,11 @@ function updateProgressIndicator(): void {
111132 cppcheckProgressIndicator . show ( ) ;
112133 // To avoid crowding status bar we alternate between progress indicator and severity option item
113134 severityOption . hide ( ) ;
135+ fullAnalysisStatusBarItem . hide ( ) ;
114136 } else {
115137 cppcheckProgressIndicator . hide ( ) ;
116138 severityOption . show ( ) ;
139+ fullAnalysisStatusBarItem . show ( ) ;
117140 }
118141}
119142
@@ -361,6 +384,69 @@ export async function activate(context: vscode.ExtensionContext) {
361384 }
362385 )
363386 ) ;
387+
388+ context . subscriptions . push (
389+ vscode . commands . registerCommand (
390+ "cppcheck-official.runFullAnalysis" ,
391+ async ( ) => {
392+
393+ const selection = await vscode . window . showQuickPick (
394+ [
395+ {
396+ label : "1 thread" ,
397+ description : "No parallel threads" ,
398+ value : "-j1"
399+ } ,
400+ {
401+ label : "2 threads" ,
402+ description : "2 parallel threads" ,
403+ value : "-j2"
404+ } ,
405+ {
406+ label : "4 threads" ,
407+ description : "4 parallel threads" ,
408+ value : "-j4"
409+ }
410+ ] ,
411+ {
412+ title : "Select how many threads to run in parallel for full analysis"
413+ }
414+ ) ;
415+ if ( ! selection ) {
416+ return ;
417+ }
418+
419+ const config = vscode . workspace . getConfiguration ( ) ;
420+ const userPath = config . get < string > ( "cppcheck-official.path" ) ?. trim ( ) || "" ;
421+ const commandPath = userPath ? resolvePath ( userPath ) : "cppcheck" ;
422+
423+ var args = config . get < string > ( "cppcheck-official.arguments" , "" ) ;
424+ const processedArgs = await processArguments ( args ) ;
425+
426+ // Check if cppcheck is available
427+ cp . exec ( `"${ commandPath } " --version` , ( error , stdout ) => {
428+ if ( error ) {
429+ vscode . window . showErrorMessage (
430+ `Cppcheck: Could not find or run '${ commandPath } '. ` +
431+ `Please install cppcheck or set 'cppcheck-official.path' correctly.`
432+ ) ;
433+ return ;
434+ }
435+ usesPremiumCppcheck = stdout . toLowerCase ( ) . includes ( 'premium' ) ;
436+ } ) ;
437+
438+ console . log ( 'processedArgs' , processedArgs ) ;
439+
440+ // Run
441+ await runFullAnalysis (
442+ commandPath ,
443+ processedArgs ,
444+ uriDiagnosticsMap ,
445+ selection . value ,
446+ ) ;
447+ }
448+ )
449+ ) ;
364450
365451 context . subscriptions . push (
366452 vscode . commands . registerCommand (
@@ -423,6 +509,12 @@ export async function activate(context: vscode.ExtensionContext) {
423509 // Call update function once at setup to set the UI text to the settings current value
424510 updateHiddenWarningTypesOption ( ) ;
425511
512+ // Full analysis status bar item
513+ fullAnalysisStatusBarItem = vscode . window . createStatusBarItem ( vscode . StatusBarAlignment . Left , 8 ) ;
514+ fullAnalysisStatusBarItem . command = "cppcheck-official.runFullAnalysis" ;
515+ fullAnalysisStatusBarItem . text = `$(play) Full Analysis` ;
516+ fullAnalysisStatusBarItem . show ( ) ;
517+ context . subscriptions . push ( fullAnalysisStatusBarItem ) ;
426518
427519 function clearDiagnosticForDoc ( doc : vscode . TextDocument ) : void {
428520 // Any file who was warnings generated from (and only from) the closed doc have their diagnostics cleared
@@ -472,22 +564,7 @@ export async function activate(context: vscode.ExtensionContext) {
472564 const commandPath = userPath ? resolvePath ( userPath ) : "cppcheck" ;
473565
474566 var args = config . get < string > ( "cppcheck-official.arguments" , "" ) ;
475- // If user enter arguments as array we parse them into space separated string format
476- if ( args . startsWith ( "[" ) && args . endsWith ( "]" ) ) {
477- args = args . replaceAll ( "[" , "" ) . replaceAll ( "]" , "" ) . replaceAll ( "," , " " ) ;
478- }
479-
480- var processedArgs = '' ;
481- // If argument field contains command to run script we do so here
482- if ( args . includes ( '@(' ) ) {
483- const scriptCommand = args . split ( "@(" ) [ 1 ] . split ( ")" ) [ 0 ] ;
484- const scriptOutput = await runCommand ( scriptCommand ) ;
485- // We expect that the script output that is to be used as arguments will be wrapped with ${}
486- const scriptOutputTrimmed = scriptOutput . split ( "@(" ) [ 1 ] . split ( ")" ) [ 0 ] ;
487- processedArgs = args . split ( "@(" ) [ 0 ] + scriptOutputTrimmed + args . split ( ")" ) ?. [ 1 ] ;
488- } else {
489- processedArgs = args ;
490- }
567+ const processedArgs = await processArguments ( args ) ;
491568
492569 // If disabled, clear any existing diagnostics for this doc.
493570 if ( ! isEnabled ) {
@@ -832,5 +909,217 @@ async function runCppcheckOnFileXML(
832909 updateProgressIndicator ( ) ;
833910}
834911
912+ async function runFullAnalysis (
913+ commandPath : string ,
914+ processedArgs : string ,
915+ uriDiagnosticsMap : Map < string , vscode . Diagnostic [ ] > ,
916+ threadsOption : string ,
917+ ) : Promise < void > {
918+ if ( ! processedArgs . includes ( "--project=" ) ) {
919+ throw new Error ( "full analysis called without specified project file!" ) ;
920+ }
921+
922+ checksRunning = true ;
923+ updateProgressIndicator ( ) ;
924+
925+ // Clear existing diagnostics for all files
926+ uriDiagnosticsMap . clear ( ) ;
927+
928+ // We always call cppcheck with severity level info, and then filter warnings when displaying them
929+ const minSevNum = SeverityNumber . Info ;
930+
931+ // Resolve paths for arguments where applicable
932+ const argsParsed = processedArgs . split ( " " ) . map ( ( arg ) => {
933+ let cleanedArg = arg . replaceAll ( "\"" , "" ) ;
934+ const isPathArgument = pathVariableArgs . some ( a => cleanedArg . startsWith ( a ) ) ;
935+ // Some arguments such as addon may be either a path or the name of a built in addon
936+ if ( isPathArgument && looksLikePath ( cleanedArg ) ) {
937+ const splitArg = cleanedArg . split ( '=' ) ;
938+ return `${ splitArg [ 0 ] } =${ resolvePath ( splitArg [ 1 ] ) } ` ;
939+ }
940+ return arg ;
941+ } ) ;
942+
943+ let usingProjectFile = true ;
944+ var projectFilePath = processedArgs . split ( '--project=' ) [ 1 ] . split ( ' ' ) [ 0 ] ;
945+ projectFileStore . clear ( ) ;
946+ projectFileStore . setUri ( vscode . Uri . file ( projectFilePath ) ) ;
947+
948+ const args = [
949+ '--enable=all' ,
950+ '--inline-suppr' ,
951+ '--xml' ,
952+ threadsOption ,
953+ ...argsParsed ,
954+ ] . filter ( Boolean ) ;
955+
956+ if ( usesPremiumCppcheck ) {
957+ args . push ( '--premium=safety-off' ) ;
958+ }
959+
960+ let proc ;
961+ const cwd = findWorkspaceRoot ( ) ;
962+ proc = cp . spawn ( commandPath , args , {
963+ cwd,
964+ } ) ;
965+
966+ await new Promise < void > ( ( resolve , reject ) => {
967+ // if spawn fails (e.g. ENOENT or permission denied)
968+ proc . on ( "error" , ( err ) => {
969+ console . error ( "Failed to start cppcheck:" , err ) ;
970+ vscode . window . showErrorMessage ( `Cppcheck failed to start: ${ err . message } ` ) ;
971+ reject ( err ) ;
972+ } ) ;
973+
974+ let xmlOutput = "" ;
975+ let out = "" ;
976+ proc . stderr . on ( "data" , d => xmlOutput += d . toString ( ) ) ;
977+ proc . stdout . on ( "data" , d => out += d . toString ( ) ) ;
978+ proc . on ( "close" , code => {
979+ if ( code && code > 0 ) {
980+ // Non-zero code means an error has occured
981+ let errorMessage = `Cppcheck failed with code ${ code } (unknown error)` ;
982+ if ( out . trim ( ) . length > 0 ) {
983+ errorMessage = out . trim ( ) ;
984+ }
985+ errorMessage = `${ errorMessage } , Command: ${ commandPath } ${ args . join ( ' ' ) } ` ;
986+ vscode . window . showErrorMessage ( errorMessage ) ;
987+ }
988+ const parser = new xml2js . Parser ( { explicitArray : true } ) ;
989+ parser . parseString ( xmlOutput , async ( err , result ) => {
990+ if ( err ) {
991+ console . error ( "XML parse error:" , err ) ;
992+ return ;
993+ }
994+
995+ const errors = result . results ?. errors ?. [ 0 ] ?. error || [ ] ;
996+ const diagnostics : Record < string , vscode . Diagnostic [ ] > = { } ;
997+ for ( const e of errors ) {
998+ const isCriticalError = criticalWarningTypes . includes ( e . $ . id ) ;
999+ const locations = e . location || [ ] ;
1000+ if ( ! locations . length ) {
1001+ continue ;
1002+ }
1003+
1004+ const mainLoc = locations [ locations . length - 1 ] . $ ;
1005+ // If main location is not current file, we are not using a project file and warning is not critical then skip displaying warning
1006+ if ( ! isCriticalError && usingProjectFile ) {
1007+ continue ;
1008+ }
1009+
1010+ let mainLocDocument : vscode . TextDocument | undefined ;
1011+ try {
1012+ mainLocDocument = await vscode . workspace . openTextDocument ( mainLoc . file ) ;
1013+ } catch {
1014+ // do nothing
1015+ }
1016+
1017+ // Cppcheck line number is 1-indexed, while VS Code uses 0-indexing
1018+ let line = Number ( mainLoc . line ) - 1 ;
1019+ // Invalid line number usually means non-analysis output
1020+ if ( isNaN ( line ) || line < 0 || ( mainLocDocument && line >= mainLocDocument . lineCount ) ) {
1021+ if ( isCriticalError ) {
1022+ line = 0 ;
1023+ } else {
1024+ continue ;
1025+ }
1026+ }
1027+
1028+ // Cppcheck col number is 1-indexed, while VS Code uses 0-indexing
1029+ let col = Number ( mainLoc . column ) - 1 ;
1030+ if ( isNaN ( col ) || col < 0 || ! mainLocDocument || col > mainLocDocument . lineAt ( line ) . text . length ) {
1031+ col = 0 ;
1032+ }
1033+
1034+ const severity = parseSeverity ( e . $ . severity ) ;
1035+ if ( ! isCriticalError && severityToNumber ( severity ) < minSevNum ) {
1036+ continue ;
1037+ }
1038+
1039+ const range = new vscode . Range ( line , col , line , mainLocDocument ? mainLocDocument . lineAt ( line ) . text . length : col ) ;
1040+ const diagnostic = new vscode . Diagnostic ( range , e . $ . msg , severity ) ;
1041+ diagnostic . source = "cppcheck" ;
1042+ // If we have a link to documentation, include it
1043+ diagnostic . code = documentationLinkMap [ e . $ . id ] ? {
1044+ value : e . $ . id ,
1045+ target : vscode . Uri . parse ( documentationLinkMap [ e . $ . id ] )
1046+ } : getPremiumCertLink ( e . $ . id ) ? {
1047+ value : e . $ . id ,
1048+ target : vscode . Uri . parse ( getPremiumCertLink ( e . $ . id ) )
1049+ } : e . $ . id ;
1050+
1051+ // If warning has a symbol we keep track of it
1052+ const symbolName = e . symbol ?. [ 0 ] ?? '' ;
1053+ // Save line of code at main location if we can access it
1054+ const mainLocLine = mainLocDocument ?. lineAt ( line ) ?. text ?? '' ;
1055+
1056+ diagnosticMetadataStore . set ( diagnostic , { symbolName, mainLocLine, hidden : false } ) ;
1057+
1058+ // Related Information
1059+ const relatedInfos : vscode . DiagnosticRelatedInformation [ ] = [ ] ;
1060+ for ( let i = 1 ; i <= locations . length ; i ++ ) {
1061+ // Related information is ordered in reverse in XML object
1062+ const loc = locations [ locations . length - i ] . $ ;
1063+ const msg = loc . info ;
1064+ const lLine = Number ( loc . line ) - 1 ;
1065+ const lCol = Number ( loc . col ) - 1 ;
1066+
1067+ if ( msg === null || msg === undefined || isNaN ( lLine ) || lLine < 0 || ( mainLocDocument && lLine >= mainLocDocument . lineCount ) ) {
1068+ continue ;
1069+ }
1070+
1071+ var relatedDocument : vscode . TextDocument | undefined ;
1072+ try {
1073+ relatedDocument = await vscode . workspace . openTextDocument ( loc . file ) ;
1074+ } catch {
1075+ // Do nothing
1076+ }
1077+ const relatedRange = new vscode . Range (
1078+ lLine , lCol ,
1079+ lLine , relatedDocument ? relatedDocument . lineAt ( lLine ) . text . length : lCol
1080+ ) ;
1081+ relatedInfos . push (
1082+ new vscode . DiagnosticRelatedInformation (
1083+ new vscode . Location ( relatedDocument ? relatedDocument . uri : vscode . Uri . file ( '' ) , relatedRange ) ,
1084+ msg
1085+ )
1086+ ) ;
1087+ }
1088+ if ( relatedInfos . length > 0 ) {
1089+ diagnostic . relatedInformation = relatedInfos ;
1090+ }
1091+ var relatedDocument : vscode . TextDocument | undefined ;
1092+ try {
1093+ relatedDocument = await vscode . workspace . openTextDocument ( mainLoc . file ) ;
1094+ } catch {
1095+ // Do nothing
1096+ }
1097+ if ( relatedDocument ) {
1098+ // Proceed if we are able to open the document
1099+ const uri = relatedDocument . uri . toString ( ) ;
1100+ if ( diagnostics [ uri ] === null || diagnostics [ uri ] === undefined ) {
1101+ diagnostics [ uri ] = [ ] ;
1102+ }
1103+ diagnostics [ uri ] . push ( diagnostic ) ;
1104+ }
1105+ }
1106+ for ( const uri of Object . keys ( diagnostics ) ) {
1107+ var newDiagnostics = diagnostics [ uri ] ;
1108+ // If file has existing diagnostics from analyzing other files we do not want to overwrite those
1109+ const existingDiagnostics = uriDiagnosticsMap . get ( uri ) ;
1110+ if ( existingDiagnostics ) {
1111+ newDiagnostics = diagnosticsUnion ( newDiagnostics , existingDiagnostics . flat ( ) ) ;
1112+ }
1113+ uriDiagnosticsMap . set ( uri , newDiagnostics ) ;
1114+ }
1115+ resolve ( ) ;
1116+ } ) ;
1117+ } ) ;
1118+ } ) ;
1119+
1120+ checksRunning = false ;
1121+ updateProgressIndicator ( ) ;
1122+ }
1123+
8351124// This method is called when your extension is deactivated
8361125export function deactivate ( ) { }
0 commit comments