diff --git a/src/lib.rs b/src/lib.rs index b73725a0fe2..3546a5f929c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -238,7 +238,7 @@ fn run_rustpython(vm: &VirtualMachine, run_mode: RunMode) -> PyResult<()> { RunMode::Module(_) => env::current_dir() .ok() .and_then(|p| p.to_str().map(|s| s.to_owned())), - RunMode::Script(_) | RunMode::InstallPip(_) => None, // handled by run_script + RunMode::Script(_) | RunMode::InstallPip(_) | RunMode::CompileOnly(_) => None, // handled by run_script RunMode::Repl => Some(String::new()), }; @@ -287,6 +287,42 @@ fn run_rustpython(vm: &VirtualMachine, run_mode: RunMode) -> PyResult<()> { run_file(vm, scope.clone(), &script_path) } RunMode::Repl => Ok(()), + RunMode::CompileOnly(files) => { + debug!("Compiling {} file(s)", files.len()); + if files.is_empty() { + eprintln!("No files specified for --compile-only"); + let exit_code = vm.ctx.new_int(1); + return Err(vm.new_exception( + vm.ctx.exceptions.system_exit.to_owned(), + vec![exit_code.into()], + )); + } + let mut success = true; + for file in files { + match std::fs::read_to_string(&file) { + Ok(source) => { + if let Err(err) = + vm.compile(&source, vm::compiler::Mode::Exec, file.clone()) + { + eprintln!("Error compiling {file}: {err}"); + success = false; + } + } + Err(err) => { + eprintln!("Error reading {file}: {err}"); + success = false; + } + } + } + if !success { + let exit_code = vm.ctx.new_int(1); + return Err(vm.new_exception( + vm.ctx.exceptions.system_exit.to_owned(), + vec![exit_code.into()], + )); + } + Ok(()) + } }; let result = if is_repl || vm.state.config.settings.inspect { shell::run_shell(vm, scope) @@ -376,4 +412,41 @@ mod tests { })()); }) } + + #[test] + fn test_compile_only() { + interpreter().enter(|vm| { + let valid = "def foo(x, y):\n return x + y\n"; + assert!( + vm.compile(valid, vm::compiler::Mode::Exec, "".to_owned()) + .is_ok() + ); + + let syntax_error = "def foo(:\n"; + assert!( + vm.compile(syntax_error, vm::compiler::Mode::Exec, "".to_owned()) + .is_err() + ); + + let duplicate_param = "def foo(x, x):\n pass\n"; + assert!( + vm.compile( + duplicate_param, + vm::compiler::Mode::Exec, + "".to_owned() + ) + .is_err() + ); + + let break_outside_loop = "def foo():\n break\n"; + assert!( + vm.compile( + break_outside_loop, + vm::compiler::Mode::Exec, + "".to_owned() + ) + .is_err() + ); + }) + } } diff --git a/src/settings.rs b/src/settings.rs index 1847e22c2d4..4d7fb1c93b1 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -10,6 +10,8 @@ pub enum RunMode { Module(String), InstallPip(InstallPipMode), Repl, + /// Compile files without executing them. Used for syntax/compile validation. + CompileOnly(Vec), } pub enum InstallPipMode { @@ -100,7 +102,8 @@ Options (and corresponding environment variables): --help-all: print complete help information and exit RustPython extensions: - +--compile-only file ... : compile files without executing (terminates option list) +--install-pip [ensurepip|get-pip] : install pip using specified method Arguments: file : program read from script file @@ -150,6 +153,13 @@ fn parse_args() -> Result<(CliArgs, RunMode, Vec), lexopt::Error> { Long("check-hash-based-pycs") => { args.check_hash_based_pycs = parser.value()?.parse()? } + Long("compile-only") => { + let files: Vec = parser + .raw_args()? + .map(|a| a.string()) + .collect::>()?; + return Ok((args, RunMode::CompileOnly(files), vec![])); + } // TODO: make these more specific Long("help-env") => help(parser),