2024-04-17 16:46:21 -04:00
use anyhow ::{ anyhow , bail , Context , Error , Result } ;
2024-04-17 12:19:08 -04:00
use std ::{
cmp ::Ordering ,
2024-04-17 16:46:21 -04:00
fs ::{ self , read_dir , OpenOptions } ,
io ::Read ,
path ::{ Path , PathBuf } ,
2024-04-17 12:19:08 -04:00
} ;
2024-04-15 17:54:57 -04:00
2024-04-17 09:55:50 -04:00
use crate ::{
2024-04-21 14:22:01 -04:00
cargo_toml ::{ append_bins , bins_start_end_ind } ,
2024-04-17 09:55:50 -04:00
info_file ::{ ExerciseInfo , InfoFile } ,
2024-04-21 13:26:19 -04:00
CURRENT_FORMAT_VERSION , DEBUG_PROFILE ,
2024-04-17 09:55:50 -04:00
} ;
2024-04-15 21:30:28 -04:00
2024-05-01 13:47:35 -04:00
// Find a char that isn't allowed in the exercise's `name` or `dir`.
2024-04-17 12:19:08 -04:00
fn forbidden_char ( input : & str ) -> Option < char > {
input . chars ( ) . find ( | c | * c ! = '_' & & ! c . is_alphanumeric ( ) )
}
2024-05-01 13:47:35 -04:00
// Check the info of all exercises and return their paths in a set.
2024-04-17 12:19:08 -04:00
fn check_info_file_exercises ( info_file : & InfoFile ) -> Result < hashbrown ::HashSet < PathBuf > > {
let mut names = hashbrown ::HashSet ::with_capacity ( info_file . exercises . len ( ) ) ;
let mut paths = hashbrown ::HashSet ::with_capacity ( info_file . exercises . len ( ) ) ;
2024-04-17 12:59:40 -04:00
2024-04-17 16:46:21 -04:00
let mut file_buf = String ::with_capacity ( 1 < < 14 ) ;
2024-04-17 12:19:08 -04:00
for exercise_info in & info_file . exercises {
2024-05-01 13:16:59 -04:00
let name = exercise_info . name . as_str ( ) ;
if name . is_empty ( ) {
2024-04-17 13:12:10 -04:00
bail! ( " Found an empty exercise name in `info.toml` " ) ;
}
2024-05-01 13:16:59 -04:00
if let Some ( c ) = forbidden_char ( name ) {
bail! ( " Char `{c}` in the exercise name `{name}` is not allowed " ) ;
2024-04-17 12:19:08 -04:00
}
if let Some ( dir ) = & exercise_info . dir {
2024-04-17 13:12:10 -04:00
if dir . is_empty ( ) {
2024-05-01 13:16:59 -04:00
bail! ( " The exercise `{name}` has an empty dir name in `info.toml` " ) ;
2024-04-17 13:12:10 -04:00
}
2024-04-17 12:19:08 -04:00
if let Some ( c ) = forbidden_char ( dir ) {
bail! ( " Char `{c}` in the exercise dir `{dir}` is not allowed " ) ;
}
}
2024-04-17 13:16:48 -04:00
if exercise_info . hint . trim ( ) . is_empty ( ) {
2024-05-01 13:16:59 -04:00
bail! ( " The exercise `{name}` has an empty hint. Please provide a hint or at least tell the user why a hint isn't needed for this exercise " ) ;
2024-04-17 13:12:10 -04:00
}
2024-05-01 13:16:59 -04:00
if ! names . insert ( name ) {
bail! ( " The exercise name `{name}` is duplicated. Exercise names must all be unique " ) ;
2024-04-17 12:19:08 -04:00
}
2024-04-17 16:46:21 -04:00
let path = exercise_info . path ( ) ;
OpenOptions ::new ( )
. read ( true )
. open ( & path )
. with_context ( | | format! ( " Failed to open the file {path} " ) ) ?
. read_to_string ( & mut file_buf )
. with_context ( | | format! ( " Failed to read the file {path} " ) ) ? ;
if ! file_buf . contains ( " fn main() " ) {
bail! ( " The `main` function is missing in the file `{path}`. \n Create at least an empty `main` function to avoid language server errors " ) ;
}
2024-05-01 13:16:59 -04:00
if ! exercise_info . test & & file_buf . contains ( " #[test] " ) {
bail! ( " The file `{path}` contains tests annotated with `#[test]` but the exercise `{name}` has `test = false` in the `info.toml` file " ) ;
}
2024-04-17 16:46:21 -04:00
file_buf . clear ( ) ;
paths . insert ( PathBuf ::from ( path ) ) ;
2024-04-17 12:19:08 -04:00
}
Ok ( paths )
}
2024-05-01 13:47:35 -04:00
// Check the `exercises` directory for unexpected files.
fn check_unexpected_files ( info_file_paths : & hashbrown ::HashSet < PathBuf > ) -> Result < ( ) > {
fn unexpected_file ( path : & Path ) -> Error {
anyhow! ( " Found the file `{}`. Only `README.md` and Rust files related to an exercise in `info.toml` are allowed in the `exercises` directory " , path . display ( ) )
}
2024-04-17 12:59:40 -04:00
2024-04-17 12:19:08 -04:00
for entry in read_dir ( " exercises " ) . context ( " Failed to open the `exercises` directory " ) ? {
let entry = entry . context ( " Failed to read the `exercises` directory " ) ? ;
if entry . file_type ( ) . unwrap ( ) . is_file ( ) {
let path = entry . path ( ) ;
let file_name = path . file_name ( ) . unwrap ( ) ;
if file_name = = " README.md " {
continue ;
}
if ! info_file_paths . contains ( & path ) {
2024-04-17 16:46:21 -04:00
return Err ( unexpected_file ( & path ) ) ;
2024-04-17 12:19:08 -04:00
}
continue ;
}
let dir_path = entry . path ( ) ;
for entry in read_dir ( & dir_path )
. with_context ( | | format! ( " Failed to open the directory {} " , dir_path . display ( ) ) ) ?
{
let entry = entry
. with_context ( | | format! ( " Failed to read the directory {} " , dir_path . display ( ) ) ) ? ;
let path = entry . path ( ) ;
if ! entry . file_type ( ) . unwrap ( ) . is_file ( ) {
2024-04-17 16:46:21 -04:00
bail! ( " Found `{}` but expected only files. Only one level of exercise nesting is allowed " , path . display ( ) ) ;
2024-04-17 12:19:08 -04:00
}
let file_name = path . file_name ( ) . unwrap ( ) ;
if file_name = = " README.md " {
continue ;
}
if ! info_file_paths . contains ( & path ) {
2024-04-17 16:46:21 -04:00
return Err ( unexpected_file ( & path ) ) ;
2024-04-17 12:19:08 -04:00
}
}
}
2024-04-17 16:46:21 -04:00
Ok ( ( ) )
2024-04-17 12:19:08 -04:00
}
2024-04-17 16:46:21 -04:00
fn check_exercises ( info_file : & InfoFile ) -> Result < ( ) > {
2024-04-17 12:19:08 -04:00
match info_file . format_version . cmp ( & CURRENT_FORMAT_VERSION ) {
Ordering ::Less = > bail! ( " `format_version` < {CURRENT_FORMAT_VERSION} (supported version) \n Please migrate to the latest format version " ) ,
Ordering ::Greater = > bail! ( " `format_version` > {CURRENT_FORMAT_VERSION} (supported version) \n Try updating the Rustlings program " ) ,
Ordering ::Equal = > ( ) ,
}
let info_file_paths = check_info_file_exercises ( info_file ) ? ;
2024-05-01 13:47:35 -04:00
check_unexpected_files ( & info_file_paths ) ? ;
2024-04-17 12:19:08 -04:00
Ok ( ( ) )
}
2024-05-01 13:47:35 -04:00
// Check that the Cargo.toml file is up-to-date.
2024-04-17 09:55:50 -04:00
fn check_cargo_toml (
exercise_infos : & [ ExerciseInfo ] ,
current_cargo_toml : & str ,
exercise_path_prefix : & [ u8 ] ,
) -> Result < ( ) > {
let ( bins_start_ind , bins_end_ind ) = bins_start_end_ind ( current_cargo_toml ) ? ;
let old_bins = & current_cargo_toml . as_bytes ( ) [ bins_start_ind .. bins_end_ind ] ;
let mut new_bins = Vec ::with_capacity ( 1 < < 13 ) ;
append_bins ( & mut new_bins , exercise_infos , exercise_path_prefix ) ;
if old_bins ! = new_bins {
2024-04-21 14:22:01 -04:00
if DEBUG_PROFILE {
bail! ( " The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it " ) ;
}
2024-04-25 09:58:46 -04:00
bail! ( " The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it " ) ;
2024-04-17 09:55:50 -04:00
}
Ok ( ( ) )
}
pub fn check ( ) -> Result < ( ) > {
let info_file = InfoFile ::parse ( ) ? ;
2024-04-17 16:46:21 -04:00
check_exercises ( & info_file ) ? ;
2024-04-15 21:30:28 -04:00
2024-05-01 13:47:35 -04:00
// A hack to make `cargo run -- dev check` work when developing Rustlings.
2024-04-21 13:26:19 -04:00
if DEBUG_PROFILE {
2024-04-17 09:55:50 -04:00
check_cargo_toml (
& info_file . exercises ,
2024-04-25 13:58:55 -04:00
include_str! ( " ../../dev-Cargo.toml " ) ,
2024-04-17 09:55:50 -04:00
b " ../ " ,
2024-04-21 14:22:01 -04:00
) ? ;
2024-04-17 09:55:50 -04:00
} else {
let current_cargo_toml =
fs ::read_to_string ( " Cargo.toml " ) . context ( " Failed to read the file `Cargo.toml` " ) ? ;
2024-04-21 14:22:01 -04:00
check_cargo_toml ( & info_file . exercises , & current_cargo_toml , b " " ) ? ;
2024-04-17 09:55:50 -04:00
}
2024-04-15 21:30:28 -04:00
2024-04-15 21:35:23 -04:00
println! ( " \n Everything looks fine! " ) ;
2024-04-15 21:30:28 -04:00
Ok ( ( ) )
2024-04-15 17:54:57 -04:00
}