#![allow(clippy::mutable_key_type)]
use std::{
env, fs,
path::{Path, PathBuf},
rc::Rc,
};
use crate::preprocess::{parse_java_ast, preprocess_asts};
use crate::pyi::generate_pyi_by_package;
mod index_tree;
mod preprocess;
mod pyi;
mod status;
fn main() {
if std::env::var("RUST_LOG").is_err() {
unsafe {
std::env::set_var("RUST_LOG", "info");
}
}
env_logger::init();
let options = match parse_args(env::args().collect()) {
Ok(options) => options,
Err(message) => {
if message != "help requested" {
eprintln!("{}", message);
eprintln!();
}
eprintln!("{}", usage());
return;
}
};
let files = match collect_java_files(&options.inputs, &options.excludes) {
Ok(files) => files,
Err(message) => {
eprintln!("{}", message);
return;
}
};
if files.is_empty() {
status::clear();
eprintln!("no .java files found in provided inputs");
return;
}
let total_files = files.len();
status::update(&format!("Parsing 0/{}", total_files));
let mut asts = Vec::new();
for (index, file) in files.iter().enumerate() {
let display_name = file
.file_name()
.and_then(|name| name.to_str())
.map(str::to_string)
.unwrap_or_else(|| file.to_string_lossy().to_string());
status::update(&format!(
"Parsing {}/{}: {}",
index + 1,
total_files,
display_name
));
match parse_java_ast(file) {
Ok(ast) => asts.push(Rc::new(ast)),
Err(e) => match &e.inner {
java_ast_parser::Error::UnrecognizedEof { .. } => {}
_ => {
status::clear();
eprintln!("failed to parse {}\n{}", file.display(), e);
return;
}
},
}
}
if asts.is_empty() {
status::clear();
eprintln!("no parsable .java files found");
return;
}
preprocess_asts(&asts);
let outputs = generate_pyi_by_package(&asts);
let total_outputs = outputs.len();
status::update(&format!("Writing 0/{}", total_outputs));
for (index, (package, contents)) in outputs.into_iter().enumerate() {
let label = if package.is_empty() {
"<root>"
} else {
package.as_str()
};
status::update(&format!(
"Writing {}/{}: {}",
index + 1,
total_outputs,
label
));
let file_path = package_to_path(&options.out_dir, &package);
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(&file_path, contents).unwrap();
ensure_parent_inits(&file_path, &options.out_dir).unwrap();
}
status::clear();
}
fn package_to_path(out_dir: &Path, package: &str) -> PathBuf {
let mut path = PathBuf::from(out_dir);
if package.is_empty() {
path.push("__init__.pyi");
return path;
}
for part in package.split('.') {
path.push(part);
}
path.push("__init__.pyi");
path
}
fn ensure_parent_inits(file_path: &Path, out_dir: &Path) -> std::io::Result<()> {
let mut current = file_path.parent();
while let Some(dir) = current {
if dir == out_dir {
break;
}
let init_path = dir.join("__init__.py");
if init_path != file_path && !init_path.exists() {
fs::write(&init_path, "")?;
}
current = dir.parent();
}
Ok(())
}
struct CliOptions {
inputs: Vec<PathBuf>,
out_dir: PathBuf,
excludes: Vec<PathBuf>,
}
fn parse_args(args: Vec<String>) -> Result<CliOptions, String> {
let mut inputs = Vec::new();
let mut out_dir = PathBuf::from("out");
let mut excludes = Vec::new();
let mut iter = args.into_iter();
let _program = iter.next();
while let Some(arg) = iter.next() {
match arg.as_str() {
"-i" | "--input" => {
let value = iter
.next()
.ok_or_else(|| "missing value for --input".to_string())?;
inputs.push(PathBuf::from(value));
}
"-o" | "--out" => {
let value = iter
.next()
.ok_or_else(|| "missing value for --out".to_string())?;
out_dir = PathBuf::from(value);
}
"-x" | "--exclude" => {
let value = iter
.next()
.ok_or_else(|| "missing value for --exclude".to_string())?;
excludes.push(PathBuf::from(value));
}
"-h" | "--help" => {
return Err(String::from("help requested"));
}
_ => {
if arg.starts_with('-') {
return Err(format!("unknown option: {}", arg));
}
inputs.push(PathBuf::from(arg));
}
}
}
if inputs.is_empty() {
return Err(String::from("no inputs provided"));
}
Ok(CliOptions {
inputs,
out_dir,
excludes,
})
}
fn usage() -> String {
[
"Usage:",
" java2pyi -i <path> [-i <path> ...] [-x <path> ...] [--out <dir>]",
"",
"Options:",
" -i, --input <path> Input file or directory (recurses for .java)",
" -x, --exclude <path> Exclude file or directory (repeatable)",
" -o, --out <dir> Output directory (default: out)",
" -h, --help Show this help",
]
.join("\n")
}
fn collect_java_files(inputs: &[PathBuf], excludes: &[PathBuf]) -> Result<Vec<PathBuf>, String> {
let mut files = Vec::new();
for input in inputs {
if is_excluded(input, excludes) {
continue;
}
let metadata = fs::metadata(input)
.map_err(|err| format!("failed to read {}: {}", input.display(), err))?;
if metadata.is_file() {
if is_java_file(input) {
files.push(input.clone());
}
} else if metadata.is_dir() {
collect_java_files_in_dir(input, &mut files, excludes)?;
}
}
Ok(files)
}
fn collect_java_files_in_dir(
dir: &Path,
files: &mut Vec<PathBuf>,
excludes: &[PathBuf],
) -> Result<(), String> {
for entry in
fs::read_dir(dir).map_err(|err| format!("failed to read {}: {}", dir.display(), err))?
{
let entry = entry.map_err(|err| format!("failed to read {}: {}", dir.display(), err))?;
let path = entry.path();
if is_excluded(&path, excludes) {
continue;
}
let metadata = entry
.metadata()
.map_err(|err| format!("failed to read {}: {}", path.display(), err))?;
if metadata.is_dir() {
collect_java_files_in_dir(&path, files, excludes)?;
} else if metadata.is_file() && is_java_file(&path) {
files.push(path);
}
}
Ok(())
}
fn is_java_file(path: &Path) -> bool {
path.extension().and_then(|ext| ext.to_str()) == Some("java")
}
fn is_excluded(path: &Path, excludes: &[PathBuf]) -> bool {
excludes.iter().any(|exclude| path.starts_with(exclude))
}