pub(in crate::print) fn looks_like_code_block_decl(line: &str) -> bool {
if line.starts_with("type ")
|| line.starts_with("type alias ")
|| line.starts_with("port ")
|| line.starts_with("infix ")
{
return true;
}
let mut chars = line.chars();
let first = match chars.next() {
Some(c) => c,
None => return false,
};
if !first.is_ascii_lowercase() && first != '_' {
return false;
}
let mut idx = first.len_utf8();
while idx < line.len() {
let c = line.as_bytes()[idx] as char;
if c.is_ascii_alphanumeric() || c == '_' || c == '\'' {
idx += 1;
} else {
break;
}
}
let rest = &line[idx..];
if rest.starts_with(" : ") || rest == " :" {
return true;
}
let bytes = rest.as_bytes();
let mut depth_round: i32 = 0;
let mut depth_square: i32 = 0;
let mut depth_curly: i32 = 0;
let mut in_string = false;
let mut in_char = false;
let mut j = 0;
while j < bytes.len() {
let b = bytes[j];
if in_string {
if b == b'\\' && j + 1 < bytes.len() {
j += 2;
continue;
}
if b == b'"' {
in_string = false;
}
j += 1;
continue;
}
if in_char {
if b == b'\\' && j + 1 < bytes.len() {
j += 2;
continue;
}
if b == b'\'' {
in_char = false;
}
j += 1;
continue;
}
match b {
b'"' => in_string = true,
b'\'' => in_char = true,
b'(' => depth_round += 1,
b')' => depth_round -= 1,
b'[' => depth_square += 1,
b']' => depth_square -= 1,
b'{' => depth_curly += 1,
b'}' => depth_curly -= 1,
b'=' if depth_round == 0 && depth_square == 0 && depth_curly == 0 => {
let prev = if j > 0 { bytes[j - 1] as char } else { ' ' };
let next = if j + 1 < bytes.len() {
bytes[j + 1] as char
} else {
' '
};
if prev != '=' && prev != '/' && prev != '>' && prev != '<' && next != '=' {
return true;
}
}
_ => {}
}
j += 1;
}
false
}
pub(in crate::print) fn looks_like_value_decl_start(line: &str) -> bool {
let bytes = line.as_bytes();
if bytes.is_empty() {
return false;
}
let first = bytes[0];
if !(first.is_ascii_lowercase() || first == b'_') {
return false;
}
let mut i = 0;
while i < bytes.len()
&& (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_' || bytes[i] == b'\'')
{
i += 1;
}
if i == 0 || i >= bytes.len() {
return false;
}
if bytes[i] != b' ' {
return false;
}
let mut depth = 0i32;
let mut in_str = false;
let mut in_char = false;
let mut esc = false;
while i < bytes.len() {
let b = bytes[i];
if esc {
esc = false;
i += 1;
continue;
}
if in_str {
if b == b'\\' {
esc = true;
} else if b == b'"' {
in_str = false;
}
i += 1;
continue;
}
if in_char {
if b == b'\\' {
esc = true;
} else if b == b'\'' {
in_char = false;
}
i += 1;
continue;
}
match b {
b'"' => in_str = true,
b'\'' => in_char = true,
b'(' | b'[' | b'{' => depth += 1,
b')' | b']' | b'}' => depth -= 1,
b'=' if depth == 0 => {
let prev = if i > 0 { bytes[i - 1] } else { b' ' };
let next = if i + 1 < bytes.len() {
bytes[i + 1]
} else {
b' '
};
if prev != b' ' {
i += 1;
continue;
}
if next == b'=' {
i += 1;
continue;
}
return true;
}
_ => {}
}
i += 1;
}
false
}
pub(in crate::print) fn looks_like_type_annotation(line: &str) -> bool {
let bytes = line.as_bytes();
let mut in_string = false;
let mut escape = false;
let mut i = 0;
while i < bytes.len() {
let c = bytes[i];
if escape {
escape = false;
} else if in_string {
if c == b'\\' {
escape = true;
} else if c == b'"' {
in_string = false;
}
} else if c == b'"' {
in_string = true;
} else if c == b':'
&& i + 1 < bytes.len()
&& bytes[i + 1] == b' '
&& i > 0
&& bytes[i - 1] == b' '
{
return true;
}
i += 1;
}
false
}
pub(in crate::print) fn is_assertion_only_paragraph(para: &[String]) -> bool {
let non_empty: Vec<&String> = para.iter().filter(|l| !l.trim().is_empty()).collect();
if non_empty.len() < 2 {
return false;
}
let mut assertion_count = 0usize;
for line in &non_empty {
if line.starts_with(' ') || line.starts_with('\t') {
return false;
}
let trimmed = line.trim();
if trimmed.starts_with("--") {
continue;
}
if !looks_like_assertion(trimmed) {
return false;
}
assertion_count += 1;
}
assertion_count >= 1
}
pub(in crate::print) fn looks_like_assertion(trimmed: &str) -> bool {
if trimmed.starts_with("--") {
return false;
}
if let Some(eq) = trimmed.find(" == ") {
let (left, right) = (&trimmed[..eq], &trimmed[eq + 4..]);
if left.is_empty() || right.is_empty() {
return false;
}
if right.starts_with('=') {
return false;
}
let last_ch = left.chars().last().unwrap();
if "+-*/|&<>".contains(last_ch) {
return false;
}
return true;
}
if let Some(dash) = trimmed.find(" -- ") {
let left = &trimmed[..dash];
if left.is_empty() {
return false;
}
let last_ch = left.chars().last().unwrap();
if "+-*/|&<>=".contains(last_ch) {
return false;
}
return true;
}
looks_like_simple_expr_line(trimmed)
}
pub(in crate::print) fn looks_like_simple_expr_line(trimmed: &str) -> bool {
let first = match trimmed.chars().next() {
Some(c) => c,
None => return false,
};
if !(first.is_ascii_alphabetic()
|| first.is_ascii_digit()
|| first == '_'
|| first == '('
|| first == '['
|| first == '\''
|| first == '"'
|| first == '-')
{
return false;
}
if first == '-' {
let second = trimmed.chars().nth(1);
match second {
Some(c) if c.is_ascii_digit() || c == '(' => {}
_ => return false,
}
}
let first_word_end = trimmed
.find(|c: char| !c.is_ascii_alphanumeric() && c != '_' && c != '.')
.unwrap_or(trimmed.len());
let first_word = &trimmed[..first_word_end];
match first_word {
"type" | "port" | "module" | "import" | "let" | "in" | "if" | "then" | "else" | "case"
| "of" | "where" | "alias" | "exposing" | "as" | "effect" | "infix" => return false,
_ => {}
}
let mut paren = 0i32;
let mut bracket = 0i32;
let mut in_str = false;
let mut in_char = false;
let mut esc = false;
for c in trimmed.chars() {
if esc {
esc = false;
continue;
}
if in_str {
if c == '\\' {
esc = true;
} else if c == '"' {
in_str = false;
}
continue;
}
if in_char {
if c == '\\' {
esc = true;
} else if c == '\'' {
in_char = false;
}
continue;
}
match c {
'"' => in_str = true,
'\'' => in_char = true,
'(' => paren += 1,
')' => {
paren -= 1;
if paren < 0 {
return false;
}
}
'[' => bracket += 1,
']' => {
bracket -= 1;
if bracket < 0 {
return false;
}
}
_ => {}
}
}
if paren != 0 || bracket != 0 || in_str || in_char {
return false;
}
let last_non_ws = trimmed.trim_end();
if let Some(lc) = last_non_ws.chars().last()
&& "+-*/|&<>=,:".contains(lc)
{
return false;
}
true
}
pub(in crate::print) fn block_has_single_line_if(block_lines: &[&str]) -> bool {
for &line in block_lines {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let leading = line.len() - line.trim_start().len();
if leading < 4 {
continue;
}
if line_has_single_line_if_then_else(trimmed) {
return true;
}
}
false
}
pub(in crate::print) fn line_has_single_line_if_then_else(line: &str) -> bool {
let bytes = line.as_bytes();
let mut in_str = false;
let mut in_char = false;
let mut in_triple = false;
let mut esc = false;
let mut i = 0;
let mut saw_then = false;
let mut saw_else = false;
while i < bytes.len() {
let b = bytes[i];
if esc {
esc = false;
i += 1;
continue;
}
if in_triple {
if i + 2 < bytes.len() && &bytes[i..i + 3] == b"\"\"\"" {
in_triple = false;
i += 3;
continue;
}
i += 1;
continue;
}
if in_str {
if b == b'\\' {
esc = true;
} else if b == b'"' {
in_str = false;
}
i += 1;
continue;
}
if in_char {
if b == b'\\' {
esc = true;
} else if b == b'\'' {
in_char = false;
}
i += 1;
continue;
}
if b == b'-' && i + 1 < bytes.len() && bytes[i + 1] == b'-' {
break;
}
if i + 2 < bytes.len() && &bytes[i..i + 3] == b"\"\"\"" {
in_triple = true;
i += 3;
continue;
}
if b == b'"' {
in_str = true;
i += 1;
continue;
}
if b == b'\'' {
in_char = true;
i += 1;
continue;
}
if i + 6 <= bytes.len() && &bytes[i..i + 6] == b" then " {
saw_then = true;
}
if i + 6 <= bytes.len() && &bytes[i..i + 6] == b" else " {
saw_else = true;
}
i += 1;
}
saw_then && saw_else
}
pub(in crate::print) fn block_has_unseparated_assertions(block_lines: &[&str]) -> bool {
let mut run_assert_count = 0usize;
for &line in block_lines {
let trimmed = line.trim();
if trimmed.is_empty() {
if run_assert_count >= 2 {
return true;
}
run_assert_count = 0;
continue;
}
let leading = line.len() - line.trim_start().len();
if leading != 4 {
if run_assert_count >= 2 {
return true;
}
run_assert_count = 0;
continue;
}
if trimmed.starts_with("--") {
continue;
}
if looks_like_assertion(trimmed) {
run_assert_count += 1;
} else {
if run_assert_count >= 2 {
return true;
}
run_assert_count = 0;
}
}
run_assert_count >= 2
}
pub(in crate::print) fn is_redundant_paren_expr(trimmed: &str) -> bool {
let bytes = trimmed.as_bytes();
if bytes.len() < 4 || bytes[0] != b'(' || *bytes.last().unwrap() != b')' {
return false;
}
let mut depth = 0i32;
let mut in_str = false;
let mut in_char = false;
let mut esc = false;
let mut saw_outer_op = false;
for (i, &b) in bytes.iter().enumerate() {
if esc {
esc = false;
continue;
}
if in_str {
if b == b'\\' {
esc = true;
} else if b == b'"' {
in_str = false;
}
continue;
}
if in_char {
if b == b'\\' {
esc = true;
} else if b == b'\'' {
in_char = false;
}
continue;
}
match b {
b'"' => in_str = true,
b'\'' => in_char = true,
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 && i != bytes.len() - 1 {
return false;
}
}
b',' if depth == 1 => return false,
b'|' | b'&' | b'+' | b'*' | b'/' | b'<' | b'>' | b'=' if depth == 1 => {
saw_outer_op = true;
}
b'-' if depth == 1 && i > 1 => {
let prev = bytes[i - 1];
if prev == b' ' {
saw_outer_op = true;
}
}
_ => {}
}
}
depth == 0 && saw_outer_op
}
pub(in crate::print) fn line_has_unpadded_hex(line: &str) -> bool {
let bytes = line.as_bytes();
let mut in_str = false;
let mut in_char = false;
let mut esc = false;
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if esc {
esc = false;
i += 1;
continue;
}
if in_str {
if b == b'\\' {
esc = true;
} else if b == b'"' {
in_str = false;
}
i += 1;
continue;
}
if in_char {
if b == b'\\' {
esc = true;
} else if b == b'\'' {
in_char = false;
}
i += 1;
continue;
}
if b == b'"' {
in_str = true;
i += 1;
continue;
}
if b == b'\'' {
in_char = true;
i += 1;
continue;
}
if b == b'0' && i + 1 < bytes.len() && (bytes[i + 1] == b'x' || bytes[i + 1] == b'X') {
let prev_ok = if i == 0 {
true
} else {
let p = bytes[i - 1];
!(p.is_ascii_alphanumeric() || p == b'_')
};
if prev_ok {
let start = i + 2;
let mut j = start;
while j < bytes.len() && bytes[j].is_ascii_hexdigit() {
j += 1;
}
let width = j - start;
if width > 0 && width != 2 && width != 4 && width != 8 && width != 16 {
return true;
}
i = j;
continue;
}
}
i += 1;
}
false
}
pub(in crate::print) fn has_compact_tuple(line: &str) -> bool {
let bytes = line.as_bytes();
let mut in_str = false;
let mut in_char = false;
let mut esc = false;
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if esc {
esc = false;
i += 1;
continue;
}
if in_str {
if b == b'\\' {
esc = true;
} else if b == b'"' {
in_str = false;
}
i += 1;
continue;
}
if in_char {
if b == b'\\' {
esc = true;
} else if b == b'\'' {
in_char = false;
}
i += 1;
continue;
}
if b == b'"' {
in_str = true;
i += 1;
continue;
}
if b == b'\'' {
in_char = true;
i += 1;
continue;
}
if b == b'(' && i + 1 < bytes.len() {
let next = bytes[i + 1];
if next == b' ' || next == b')' {
i += 1;
continue;
}
let mut depth = 1i32;
let mut j = i + 1;
let mut inner_in_str = false;
let mut inner_in_char = false;
let mut inner_esc = false;
let mut found_comma = false;
while j < bytes.len() && depth > 0 {
let c = bytes[j];
if inner_esc {
inner_esc = false;
j += 1;
continue;
}
if inner_in_str {
if c == b'\\' {
inner_esc = true;
} else if c == b'"' {
inner_in_str = false;
}
j += 1;
continue;
}
if inner_in_char {
if c == b'\\' {
inner_esc = true;
} else if c == b'\'' {
inner_in_char = false;
}
j += 1;
continue;
}
match c {
b'"' => inner_in_str = true,
b'\'' => inner_in_char = true,
b'(' | b'[' | b'{' => depth += 1,
b')' | b']' | b'}' => depth -= 1,
b',' if depth == 1 => found_comma = true,
_ => {}
}
j += 1;
}
if found_comma && j > 0 {
if bytes[j - 1] == b')' {
let before_close = if j >= 2 { bytes[j - 2] } else { b' ' };
if before_close != b' ' {
return true;
}
}
}
i = j;
continue;
}
i += 1;
}
false
}
pub(in crate::print) fn line_has_sci_float_without_dot(line: &str) -> bool {
let bytes = line.as_bytes();
let mut in_str = false;
let mut in_char = false;
let mut esc = false;
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if esc {
esc = false;
i += 1;
continue;
}
if in_str {
if b == b'\\' {
esc = true;
} else if b == b'"' {
in_str = false;
}
i += 1;
continue;
}
if in_char {
if b == b'\\' {
esc = true;
} else if b == b'\'' {
in_char = false;
}
i += 1;
continue;
}
if b == b'"' {
in_str = true;
i += 1;
continue;
}
if b == b'\'' {
in_char = true;
i += 1;
continue;
}
if b.is_ascii_digit() {
let prev_ok = if i == 0 {
true
} else {
let p = bytes[i - 1];
!(p.is_ascii_alphanumeric() || p == b'_' || p == b'.')
};
if prev_ok {
let start = i;
let mut j = i;
while j < bytes.len() && bytes[j].is_ascii_digit() {
j += 1;
}
let has_dot = j < bytes.len() && bytes[j] == b'.';
if has_dot {
j += 1;
while j < bytes.len() && bytes[j].is_ascii_digit() {
j += 1;
}
}
let has_exp = j < bytes.len() && (bytes[j] == b'e' || bytes[j] == b'E');
if has_exp && !has_dot {
let mut k = j + 1;
if k < bytes.len() && (bytes[k] == b'+' || bytes[k] == b'-') {
k += 1;
}
if k < bytes.len() && bytes[k].is_ascii_digit() {
let _ = start;
return true;
}
}
i = j;
continue;
}
}
i += 1;
}
false
}
pub(in crate::print) fn has_tight_binary_op(line: &str) -> bool {
let bytes = line.as_bytes();
let mut in_str = false;
let mut escape = false;
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if escape {
escape = false;
i += 1;
continue;
}
if in_str {
if b == b'\\' {
escape = true;
} else if b == b'"' {
in_str = false;
}
i += 1;
continue;
}
if b == b'"' {
in_str = true;
i += 1;
continue;
}
if b == b'^' && i > 0 && i + 1 < bytes.len() {
let prev = bytes[i - 1];
let next = bytes[i + 1];
let is_ident = |c: u8| c.is_ascii_alphanumeric() || c == b'_';
if is_ident(prev) && is_ident(next) {
return true;
}
}
if b == b'/' && i > 0 && i + 1 < bytes.len() {
let prev = bytes[i - 1];
let next = bytes[i + 1];
let is_ident = |c: u8| c.is_ascii_alphanumeric() || c == b'_';
if next == b'/' {
if i + 2 < bytes.len() {
let after = bytes[i + 2];
if is_ident(prev) && is_ident(after) {
return true;
}
}
i += 2;
continue;
}
if is_ident(prev) && is_ident(next) {
return true;
}
}
i += 1;
}
false
}
pub(in crate::print) fn import_has_unsorted_exposing(line: &str) -> bool {
let t = line.trim();
if !t.starts_with("import ") {
return false;
}
let exp_idx = match t.find(" exposing (") {
Some(i) => i,
None => return false,
};
let rest = &t[exp_idx + " exposing (".len()..];
let close_idx = match rest.rfind(')') {
Some(i) => i,
None => return false,
};
let inner = &rest[..close_idx];
if inner.trim() == ".." {
return false;
}
let items: Vec<String> = inner
.split(',')
.map(|s| {
let s = s.trim();
let head = s.split('(').next().unwrap_or(s).trim();
head.to_string()
})
.filter(|s| !s.is_empty())
.collect();
if items.len() < 2 {
return false;
}
let mut sorted = items.clone();
sorted.sort_by_key(|a| a.to_lowercase());
items != sorted
}
pub(in crate::print) fn is_single_line_value_decl(trimmed: &str) -> bool {
let first = match trimmed.chars().next() {
Some(c) => c,
None => return false,
};
if !(first.is_ascii_lowercase() || first == '_') {
return false;
}
let first_word_end = trimmed
.find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
.unwrap_or(trimmed.len());
let first_word = &trimmed[..first_word_end];
match first_word {
"type" | "port" | "module" | "import" | "let" | "in" | "if" | "then" | "else" | "case"
| "of" | "where" | "alias" | "exposing" | "as" | "effect" | "infix" => return false,
_ => {}
}
let bytes = trimmed.as_bytes();
let mut i = 0;
while i + 2 < bytes.len() {
if bytes[i] == b' ' && bytes[i + 1] == b'=' && bytes[i + 2] == b' ' {
if i > 0 {
let prev = bytes[i - 1];
if prev == b'='
|| prev == b'/'
|| prev == b'<'
|| prev == b'>'
|| prev == b'!'
|| prev == b':'
{
i += 1;
continue;
}
}
if i + 3 < bytes.len() && bytes[i + 3] == b'=' {
i += 1;
continue;
}
let left = trimmed[..i].trim();
if left.is_empty() {
return false;
}
let left_first = left.chars().next().unwrap();
if !(left_first.is_ascii_lowercase() || left_first == '_') {
return false;
}
let right = trimmed[i + 3..].trim();
if right.is_empty() {
return false;
}
return true;
}
i += 1;
}
false
}