@@ -410,6 +410,54 @@ pub struct FormatResponse {
410410 pub code : String ,
411411}
412412
413+ #[ derive( Debug , Clone ) ]
414+ pub struct ClippyRequest {
415+ pub channel : Channel ,
416+ pub crate_type : CrateType ,
417+ pub edition : Edition ,
418+ pub code : String ,
419+ }
420+
421+ impl ClippyRequest {
422+ pub ( crate ) fn delete_previous_main_request ( & self ) -> DeleteFileRequest {
423+ delete_previous_primary_file_request ( self . crate_type )
424+ }
425+
426+ pub ( crate ) fn write_main_request ( & self ) -> WriteFileRequest {
427+ write_primary_file_request ( self . crate_type , & self . code )
428+ }
429+
430+ pub ( crate ) fn execute_cargo_request ( & self ) -> ExecuteCommandRequest {
431+ ExecuteCommandRequest {
432+ cmd : "cargo" . to_owned ( ) ,
433+ args : vec ! [ "clippy" . to_owned( ) ] ,
434+ envs : Default :: default ( ) ,
435+ cwd : None ,
436+ }
437+ }
438+ }
439+
440+ impl CargoTomlModifier for ClippyRequest {
441+ fn modify_cargo_toml ( & self , mut cargo_toml : toml:: Value ) -> toml:: Value {
442+ if self . edition == Edition :: Rust2024 {
443+ cargo_toml = modify_cargo_toml:: set_feature_edition2024 ( cargo_toml) ;
444+ }
445+
446+ cargo_toml = modify_cargo_toml:: set_edition ( cargo_toml, self . edition . to_cargo_toml_key ( ) ) ;
447+
448+ if let Some ( crate_type) = self . crate_type . to_library_cargo_toml_key ( ) {
449+ cargo_toml = modify_cargo_toml:: set_crate_type ( cargo_toml, crate_type) ;
450+ }
451+ cargo_toml
452+ }
453+ }
454+
455+ #[ derive( Debug , Clone ) ]
456+ pub struct ClippyResponse {
457+ pub success : bool ,
458+ pub exit_detail : String ,
459+ }
460+
413461#[ derive( Debug , Clone ) ]
414462pub struct WithOutput < T > {
415463 pub response : T ,
@@ -574,6 +622,33 @@ where
574622 . await
575623 }
576624
625+ pub async fn clippy (
626+ & self ,
627+ request : ClippyRequest ,
628+ ) -> Result < WithOutput < ClippyResponse > , ClippyError > {
629+ use clippy_error:: * ;
630+
631+ self . select_channel ( request. channel )
632+ . await
633+ . context ( CouldNotStartContainerSnafu ) ?
634+ . clippy ( request)
635+ . await
636+ }
637+
638+ pub async fn begin_clippy (
639+ & self ,
640+ token : CancellationToken ,
641+ request : ClippyRequest ,
642+ ) -> Result < ActiveClippy , ClippyError > {
643+ use clippy_error:: * ;
644+
645+ self . select_channel ( request. channel )
646+ . await
647+ . context ( CouldNotStartContainerSnafu ) ?
648+ . begin_clippy ( token, request)
649+ . await
650+ }
651+
577652 pub async fn idle ( & mut self ) -> Result < ( ) > {
578653 let Self {
579654 stable,
@@ -929,6 +1004,78 @@ impl Container {
9291004 } )
9301005 }
9311006
1007+ async fn clippy (
1008+ & self ,
1009+ request : ClippyRequest ,
1010+ ) -> Result < WithOutput < ClippyResponse > , ClippyError > {
1011+ let token = Default :: default ( ) ;
1012+
1013+ let ActiveClippy {
1014+ task,
1015+ stdout_rx,
1016+ stderr_rx,
1017+ } = self . begin_clippy ( token, request) . await ?;
1018+
1019+ WithOutput :: try_absorb ( task, stdout_rx, stderr_rx) . await
1020+ }
1021+
1022+ async fn begin_clippy (
1023+ & self ,
1024+ token : CancellationToken ,
1025+ request : ClippyRequest ,
1026+ ) -> Result < ActiveClippy , ClippyError > {
1027+ use clippy_error:: * ;
1028+
1029+ let delete_previous_main = request. delete_previous_main_request ( ) ;
1030+ let write_main = request. write_main_request ( ) ;
1031+ let execute_cargo = request. execute_cargo_request ( ) ;
1032+
1033+ let delete_previous_main = self . commander . one ( delete_previous_main) ;
1034+ let write_main = self . commander . one ( write_main) ;
1035+ let modify_cargo_toml = self . modify_cargo_toml . modify_for ( & request) ;
1036+
1037+ let ( delete_previous_main, write_main, modify_cargo_toml) =
1038+ join ! ( delete_previous_main, write_main, modify_cargo_toml) ;
1039+
1040+ delete_previous_main. context ( CouldNotDeletePreviousCodeSnafu ) ?;
1041+ write_main. context ( CouldNotWriteCodeSnafu ) ?;
1042+ modify_cargo_toml. context ( CouldNotModifyCargoTomlSnafu ) ?;
1043+
1044+ let SpawnCargo {
1045+ task,
1046+ stdin_tx,
1047+ stdout_rx,
1048+ stderr_rx,
1049+ } = self
1050+ . spawn_cargo_task ( token, execute_cargo)
1051+ . await
1052+ . context ( CouldNotStartCargoSnafu ) ?;
1053+
1054+ drop ( stdin_tx) ;
1055+
1056+ let task = async move {
1057+ let ExecuteCommandResponse {
1058+ success,
1059+ exit_detail,
1060+ } = task
1061+ . await
1062+ . context ( CargoTaskPanickedSnafu ) ?
1063+ . context ( CargoFailedSnafu ) ?;
1064+
1065+ Ok ( ClippyResponse {
1066+ success,
1067+ exit_detail,
1068+ } )
1069+ }
1070+ . boxed ( ) ;
1071+
1072+ Ok ( ActiveClippy {
1073+ task,
1074+ stdout_rx,
1075+ stderr_rx,
1076+ } )
1077+ }
1078+
9321079 async fn spawn_cargo_task (
9331080 & self ,
9341081 token : CancellationToken ,
@@ -1158,6 +1305,47 @@ pub enum FormatError {
11581305 CodeNotUtf8 { source : std:: string:: FromUtf8Error } ,
11591306}
11601307
1308+ pub struct ActiveClippy {
1309+ pub task : BoxFuture < ' static , Result < ClippyResponse , ClippyError > > ,
1310+ pub stdout_rx : mpsc:: Receiver < String > ,
1311+ pub stderr_rx : mpsc:: Receiver < String > ,
1312+ }
1313+
1314+ impl fmt:: Debug for ActiveClippy {
1315+ fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
1316+ f. debug_struct ( "ActiveClippy" )
1317+ . field ( "task" , & "<future>" )
1318+ . field ( "stdout_rx" , & self . stdout_rx )
1319+ . field ( "stderr_rx" , & self . stderr_rx )
1320+ . finish ( )
1321+ }
1322+ }
1323+
1324+ #[ derive( Debug , Snafu ) ]
1325+ #[ snafu( module) ]
1326+ pub enum ClippyError {
1327+ #[ snafu( display( "Could not start the container" ) ) ]
1328+ CouldNotStartContainer { source : Error } ,
1329+
1330+ #[ snafu( display( "Could not modify Cargo.toml" ) ) ]
1331+ CouldNotModifyCargoToml { source : ModifyCargoTomlError } ,
1332+
1333+ #[ snafu( display( "Could not delete previous source code" ) ) ]
1334+ CouldNotDeletePreviousCode { source : CommanderError } ,
1335+
1336+ #[ snafu( display( "Could not write source code" ) ) ]
1337+ CouldNotWriteCode { source : CommanderError } ,
1338+
1339+ #[ snafu( display( "Could not start Cargo task" ) ) ]
1340+ CouldNotStartCargo { source : SpawnCargoError } ,
1341+
1342+ #[ snafu( display( "The Cargo task panicked" ) ) ]
1343+ CargoTaskPanicked { source : tokio:: task:: JoinError } ,
1344+
1345+ #[ snafu( display( "Cargo task failed" ) ) ]
1346+ CargoFailed { source : SpawnCargoError } ,
1347+ }
1348+
11611349struct SpawnCargo {
11621350 task : JoinHandle < Result < ExecuteCommandResponse , SpawnCargoError > > ,
11631351 stdin_tx : mpsc:: Sender < String > ,
@@ -1749,29 +1937,34 @@ mod tests {
17491937 let project_dir =
17501938 TempDir :: new ( "playground" ) . expect ( "Failed to create temporary project directory" ) ;
17511939
1752- let output = std:: process:: Command :: new ( "cargo" )
1753- . arg ( "init" )
1754- . args ( [ "--name" , "playground" ] )
1755- . arg ( project_dir. path ( ) )
1756- . output ( )
1757- . expect ( "Build failed" ) ;
1758- assert ! ( output. status. success( ) , "Cargo initialization failed" ) ;
1940+ for channel in Channel :: ALL {
1941+ let channel = channel. to_str ( ) ;
1942+ let channel_dir = project_dir. path ( ) . join ( channel) ;
17591943
1760- let main = project_dir. path ( ) . join ( "src" ) . join ( "main.rs" ) ;
1761- std:: fs:: remove_file ( main) . expect ( "Could not delete main.rs" ) ;
1944+ let output = std:: process:: Command :: new ( "cargo" )
1945+ . arg ( format ! ( "+{channel}" ) )
1946+ . arg ( "new" )
1947+ . args ( [ "--name" , "playground" ] )
1948+ . arg ( & channel_dir)
1949+ . output ( )
1950+ . expect ( "Cargo new failed" ) ;
1951+ assert ! ( output. status. success( ) , "Cargo new failed" ) ;
1952+
1953+ let main = channel_dir. join ( "src" ) . join ( "main.rs" ) ;
1954+ std:: fs:: remove_file ( main) . expect ( "Could not delete main.rs" ) ;
1955+ }
17621956
17631957 Self { project_dir }
17641958 }
17651959 }
17661960
17671961 impl Backend for TestBackend {
17681962 fn prepare_worker_command ( & self , channel : Channel ) -> Command {
1769- let toolchain_file = format ! ( r#"[toolchain]\nchannel = "{}""# , channel. to_str( ) ) ;
1770- let path = self . project_dir . path ( ) . join ( "rust-toolchain.toml" ) ;
1771- std:: fs:: write ( path, toolchain_file) . expect ( "Couldn't write toolchain file" ) ;
1963+ let channel_dir = self . project_dir . path ( ) . join ( channel. to_str ( ) ) ;
17721964
17731965 let mut command = Command :: new ( "./target/debug/worker" ) ;
1774- command. arg ( self . project_dir . path ( ) ) ;
1966+ command. env ( "RUSTUP_TOOLCHAIN" , channel. to_str ( ) ) ;
1967+ command. arg ( channel_dir) ;
17751968 command
17761969 }
17771970 }
@@ -2647,6 +2840,76 @@ mod tests {
26472840 Ok ( ( ) )
26482841 }
26492842
2843+ const ARBITRARY_CLIPPY_REQUEST : ClippyRequest = ClippyRequest {
2844+ channel : Channel :: Stable ,
2845+ crate_type : CrateType :: Library ( LibraryType :: Rlib ) ,
2846+ edition : Edition :: Rust2021 ,
2847+ code : String :: new ( ) ,
2848+ } ;
2849+
2850+ #[ tokio:: test]
2851+ #[ snafu:: report]
2852+ async fn clippy ( ) -> Result < ( ) > {
2853+ let coordinator = new_coordinator ( ) . await ;
2854+
2855+ let req = ClippyRequest {
2856+ code : r#"
2857+ fn main() {
2858+ let a = 0.0 / 0.0;
2859+ println!("NaN is {}", a);
2860+ }
2861+ "#
2862+ . into ( ) ,
2863+ ..ARBITRARY_CLIPPY_REQUEST
2864+ } ;
2865+
2866+ let response = coordinator. clippy ( req) . with_timeout ( ) . await . unwrap ( ) ;
2867+
2868+ assert ! ( !response. success, "stderr: {}" , response. stderr) ;
2869+ assert_contains ! ( response. stderr, "deny(clippy::eq_op)" ) ;
2870+ assert_contains ! ( response. stderr, "warn(clippy::zero_divided_by_zero)" ) ;
2871+
2872+ Ok ( ( ) )
2873+ }
2874+
2875+ #[ tokio:: test]
2876+ #[ snafu:: report]
2877+ async fn clippy_edition ( ) -> Result < ( ) > {
2878+ let cases = [ (
2879+ "#![deny(clippy::single_element_loop)]
2880+ fn a() { for i in [1] { dbg!(i); } }" ,
2881+ [ true , true , false , false ] ,
2882+ ) ] ;
2883+
2884+ let tests = cases. into_iter ( ) . flat_map ( |( code, expected_to_be_clean) | {
2885+ Edition :: ALL . into_iter ( ) . zip ( expected_to_be_clean) . map (
2886+ move |( edition, expected_to_be_clean) | async move {
2887+ let coordinator = new_coordinator ( ) . await ;
2888+
2889+ let req = ClippyRequest {
2890+ edition,
2891+ code : code. into ( ) ,
2892+ ..ARBITRARY_CLIPPY_REQUEST
2893+ } ;
2894+
2895+ let response = coordinator. clippy ( req) . with_timeout ( ) . await . unwrap ( ) ;
2896+
2897+ assert_eq ! (
2898+ response. success, expected_to_be_clean,
2899+ "{code:?} in {edition:?}, {}" ,
2900+ response. stderr
2901+ ) ;
2902+
2903+ Ok ( ( ) )
2904+ } ,
2905+ )
2906+ } ) ;
2907+
2908+ try_join_all ( tests) . with_timeout ( ) . await ?;
2909+
2910+ Ok ( ( ) )
2911+ }
2912+
26502913 // The next set of tests are broader than the functionality of a
26512914 // single operation.
26522915
0 commit comments