use super::depyler_value::extract_string_typed_vars;
pub(super) fn fix_python_truthiness(code: &str) -> String {
let bool_vars = extract_bool_typed_vars(code);
let lines: Vec<&str> = code.lines().collect();
let mut result = Vec::with_capacity(lines.len());
for line in &lines {
let fixed = fix_truthiness_line(line, &bool_vars);
result.push(fixed);
}
result.join("\n") + "\n"
}
pub(super) fn extract_bool_typed_vars(code: &str) -> Vec<String> {
let mut vars = Vec::new();
let mut vec_bool_params: Vec<String> = Vec::new();
for line in code.lines() {
let trimmed = line.trim();
if trimmed.starts_with("fn ") || trimmed.starts_with("pub fn ") {
extract_bool_vars_from_fn_sig(trimmed, &mut vars, &mut vec_bool_params);
continue;
}
if trimmed.starts_with("let ") {
extract_bool_var_from_let(trimmed, &mut vars);
}
if trimmed.starts_with("for ") {
extract_bool_var_from_for_loop(trimmed, &vec_bool_params, &mut vars);
}
}
vars
}
fn extract_bool_vars_from_fn_sig(
trimmed: &str,
vars: &mut Vec<String>,
vec_bool_params: &mut Vec<String>,
) {
let Some(start) = trimmed.find('(') else {
return;
};
let Some(end) = trimmed.find(')') else {
return;
};
let params = &trimmed[start + 1..end];
for param in params.split(',') {
let p = param.trim();
extract_bool_param(p, vars);
extract_vec_bool_param(p, vec_bool_params);
}
}
fn extract_bool_param(p: &str, vars: &mut Vec<String>) {
if !p.ends_with(": bool") {
return;
}
if let Some(name) = p.strip_suffix(": bool") {
let name = name.trim();
if !name.is_empty() {
vars.push(name.to_string());
}
}
}
fn extract_vec_bool_param(p: &str, vec_bool_params: &mut Vec<String>) {
if !p.contains("Vec<bool>") && !p.contains("Vec < bool >") {
return;
}
if let Some(colon_pos) = p.find(':') {
let name = p[..colon_pos].trim();
if !name.is_empty() {
vec_bool_params.push(name.to_string());
}
}
}
fn extract_bool_var_from_let(trimmed: &str, vars: &mut Vec<String>) {
let rest = trimmed.strip_prefix("let ").unwrap_or("");
let rest = rest.strip_prefix("mut ").unwrap_or(rest);
if let Some(colon_pos) = rest.find(": bool") {
let name = rest[..colon_pos].trim();
if !name.is_empty()
&& name
.chars()
.all(|c| c.is_alphanumeric() || c == '_')
{
vars.push(name.to_string());
}
}
}
fn extract_bool_var_from_for_loop(
trimmed: &str,
vec_bool_params: &[String],
vars: &mut Vec<String>,
) {
let Some(in_pos) = trimmed.find(" in ") else {
return;
};
let loop_var = trimmed[4..in_pos].trim();
let iter_part = &trimmed[in_pos + 4..];
for param in vec_bool_params {
let matches_param = iter_part.starts_with(&format!("{param}."))
|| iter_part.starts_with(&format!("{param} "));
let is_valid_ident = !loop_var.is_empty()
&& loop_var
.chars()
.all(|c| c.is_alphanumeric() || c == '_');
if matches_param && is_valid_ident {
vars.push(loop_var.to_string());
}
}
}
pub(super) fn fix_truthiness_line(line: &str, bool_vars: &[String]) -> String {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("if !") {
if let Some(ident) = rest.strip_suffix(" {") {
if is_likely_non_boolean_ident(ident, bool_vars) {
let indent = &line[..line.len() - line.trim_start().len()];
return format!("{}if {}.is_empty() {{", indent, ident);
}
}
}
line.to_string()
}
pub(super) fn is_likely_non_boolean_ident(ident: &str, bool_vars: &[String]) -> bool {
if ident.contains('.') || ident.contains('(') || ident.contains('[') || ident.contains(' ') {
return false;
}
if bool_vars.iter().any(|v| v == ident) {
return false;
}
let bool_prefixes = [
"is_",
"has_",
"should_",
"can_",
"will_",
"was_",
"did_",
"does_",
"are_",
"do_",
"were_",
"ok",
"err",
"found",
"done",
"valid",
"enabled",
"disabled",
"active",
"ready",
"empty",
"full",
"true",
"false",
"success",
"failed",
"_cse_temp",
];
for prefix in &bool_prefixes {
if ident.starts_with(prefix) || ident == *prefix {
return false;
}
}
ident.chars().all(|c| c.is_alphanumeric() || c == '_')
}
pub(super) fn fix_negation_on_non_bool(code: &str) -> String {
if !code.contains("!") {
return code.to_string();
}
let string_typed_vars = extract_string_typed_vars(code);
if string_typed_vars.is_empty() {
return code.to_string();
}
let bool_vars = extract_bool_typed_vars(code);
let string_typed_vars: Vec<String> = string_typed_vars
.into_iter()
.filter(|v| !bool_vars.contains(v))
.collect();
if string_typed_vars.is_empty() {
return code.to_string();
}
let mut sorted_vars = string_typed_vars.clone();
sorted_vars.sort_by_key(|b: &String| std::cmp::Reverse(b.len()));
let lines: Vec<&str> = code.lines().collect();
let mut result: Vec<String> = Vec::with_capacity(lines.len());
for line in &lines {
let fixed = replace_negation_with_is_empty(line, &sorted_vars);
result.push(fixed);
}
result.join("\n")
}
fn replace_negation_with_is_empty(line: &str, sorted_vars: &[String]) -> String {
let mut fixed = line.to_string();
for var in sorted_vars {
let neg_pattern = format!("!{}", var);
let Some(pos) = fixed.find(&neg_pattern) else {
continue;
};
let after_pos = pos + neg_pattern.len();
let next_char = fixed[after_pos..].chars().next();
let is_word_boundary = next_char
.map(|c| !c.is_alphanumeric() && c != '_' && c != '.')
.unwrap_or(true);
if is_word_boundary {
let empty_check = format!("{}.is_empty()", var);
fixed = format!("{}{}{}", &fixed[..pos], empty_check, &fixed[after_pos..]);
}
}
fixed
}
pub(super) fn fix_field_access_truthiness(code: &str) -> String {
let lines: Vec<&str> = code.lines().collect();
let mut result: Vec<String> = Vec::with_capacity(lines.len());
for line in &lines {
let fixed = fix_field_negation_in_line(line);
result.push(fixed);
}
result.join("\n")
}
pub(super) fn fix_field_negation_in_line(line: &str) -> String {
let mut result = line.to_string();
loop {
let current = result.clone();
if let Some(replacement) = find_and_replace_field_negation(¤t) {
result = replacement;
} else {
break;
}
}
result
}
pub(super) fn find_and_replace_field_negation(line: &str) -> Option<String> {
let bytes = line.as_bytes();
let len = bytes.len();
let mut i = 0;
while i < len {
if bytes[i] == b'!' {
if let Some(replacement) = try_replace_field_negation_at(line, i) {
return Some(replacement);
}
}
i += 1;
}
None
}
fn try_replace_field_negation_at(line: &str, bang_pos: usize) -> Option<String> {
let bytes = line.as_bytes();
let len = bytes.len();
if !is_valid_negation_prefix(bytes, bang_pos) {
return None;
}
let (_ident1, dot_pos) = parse_ident_before_dot(bytes, line, bang_pos + 1, len)?;
let field_start = dot_pos + 1;
let (field, field_end): (&str, usize) = parse_field_name(bytes, line, field_start, len)?;
if field_end < len && bytes[field_end] == b'(' {
return None;
}
if field_end < len && bytes[field_end] == b'.' {
return None;
}
if field.chars().all(|c| c.is_ascii_digit()) {
return None;
}
if is_likely_bool_field(field) {
return None;
}
let ident1 = &line[bang_pos + 1..dot_pos];
let replacement = format!(
"{}{}.{}.is_empty(){}",
&line[..bang_pos],
ident1,
field,
&line[field_end..]
);
Some(replacement)
}
fn is_valid_negation_prefix(bytes: &[u8], bang_pos: usize) -> bool {
bang_pos == 0
|| matches!(
bytes[bang_pos - 1],
b' ' | b'(' | b'=' | b'{' | b'|' | b'&' | b'\t'
)
}
fn parse_ident_before_dot<'a>(
bytes: &[u8],
line: &'a str,
start: usize,
len: usize,
) -> Option<(&'a str, usize)> {
let mut j = start;
while j < len && (bytes[j].is_ascii_alphanumeric() || bytes[j] == b'_') {
j += 1;
}
if j == start || j >= len || bytes[j] != b'.' {
return None;
}
Some((&line[start..j], j))
}
fn parse_field_name<'a>(
bytes: &[u8],
line: &'a str,
start: usize,
len: usize,
) -> Option<(&'a str, usize)> {
let mut j = start;
while j < len && (bytes[j].is_ascii_alphanumeric() || bytes[j] == b'_') {
j += 1;
}
if j == start {
return None;
}
Some((&line[start..j], j))
}
pub(super) fn is_likely_bool_field(field: &str) -> bool {
const BOOL_PREFIXES: &[&str] = &[
"is_",
"has_",
"should_",
"can_",
"enable",
"disable",
"use_",
"load_in_",
"allow_",
"do_",
"with_",
"no_",
"skip_",
"force_",
"apply_",
"generate_",
"include_",
"exclude_",
];
const BOOL_SUFFIXES: &[&str] = &["_enabled", "_flag", "_only"];
const BOOL_EXACT: &[&str] = &[
"verbose",
"debug",
"quiet",
"overwrite",
"resume",
"fp16",
"bf16",
];
BOOL_PREFIXES.iter().any(|p| field.starts_with(p))
|| BOOL_SUFFIXES.iter().any(|s| field.ends_with(s))
|| BOOL_EXACT.iter().any(|e| field == *e)
}
pub(super) fn fix_not_string_truthiness(code: &str) -> String {
let mut result = code.to_string();
result = fix_not_trim_to_string(&result);
result = fix_not_to_string(&result);
result
}
pub(super) fn fix_not_trim_to_string(code: &str) -> String {
let mut result = code.to_string();
let pattern = ".trim().to_string())";
while let Some(end_pos) = result.find(pattern) {
let before = &result[..end_pos];
if let Some(start) = before.rfind("(!") {
let expr = &result[start + 2..end_pos];
if expr
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '.')
{
let old = format!("(!{}{})", expr, ".trim().to_string()");
let new = format!("{}.trim().is_empty()", expr);
result = result.replacen(&old, &new, 1);
continue;
}
}
break;
}
result
}
pub(super) fn fix_not_to_string(code: &str) -> String {
let mut result = code.to_string();
let marker = ".to_string())";
let mut search_from = 0;
while search_from < result.len() {
let haystack = &result[search_from..];
let Some(rel_pos) = haystack.find(marker) else {
break;
};
let end_pos = search_from + rel_pos;
let before = &result[..end_pos];
if let Some(start) = before.rfind("(!") {
let expr = &result[start + 2..end_pos];
if expr
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '.')
{
let old = format!("(!{}.to_string())", expr);
if result[start..].starts_with(&old) {
let new = format!("{}.is_empty()", expr);
result = format!(
"{}{}{}",
&result[..start],
new,
&result[start + old.len()..]
);
search_from = start + new.len();
continue;
}
}
}
search_from = end_pos + marker.len();
}
result
}
pub(super) fn fix_bitwise_and_truthiness(code: &str) -> String {
let mut result = String::with_capacity(code.len());
for line in code.lines() {
let fixed = fix_bitwise_and_line(line);
result.push_str(&fixed);
result.push('\n');
}
if code.ends_with('\n') {
result
} else {
result.truncate(result.len().saturating_sub(1));
result
}
}
fn fix_bitwise_and_line(line: &str) -> String {
let trimmed = line.trim();
if !is_bitwise_and_candidate(trimmed) {
return line.to_string();
}
let Some(rest) = trimmed.strip_prefix("if ") else {
return line.to_string();
};
let Some(expr) = rest.strip_suffix('{') else {
return line.to_string();
};
let expr = expr.trim();
if !expr.contains(" & ") {
return line.to_string();
}
let indent = line.len() - line.trim_start().len();
let pad: String = " ".repeat(indent);
format!("{}if ({}) != 0 {{", pad, expr)
}
fn is_bitwise_and_candidate(trimmed: &str) -> bool {
trimmed.starts_with("if ")
&& trimmed.contains(" & ")
&& trimmed.ends_with('{')
&& !trimmed.contains("!=")
&& !trimmed.contains("==")
&& !trimmed.contains("&&")
&& !trimmed.contains("||")
&& !trimmed.contains("(& ")
&& !trimmed.contains(", & ")
}