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 paragraph_is_single_expr_with_line_comment(para: &[String]) -> bool {
let mut expr_lines = 0usize;
let mut comment_lines = 0usize;
for line in para {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if trimmed.starts_with("--") {
comment_lines += 1;
} else {
expr_lines += 1;
}
}
expr_lines == 1 && comment_lines >= 1
}
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 has_unterminated_string_or_char(trimmed) {
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 Some(last_ch) = left.chars().last() else {
unreachable!("left was checked non-empty earlier")
};
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 Some(last_ch) = left.chars().last() else {
unreachable!("checked !left.is_empty() above")
};
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 {
if trimmed.contains("=>") || trimmed.contains("|>>") {
return false;
}
if contains_sentence_period(trimmed) {
return false;
}
if looks_like_ordered_list_item(trimmed) {
return false;
}
if contains_bare_arrow(trimmed) {
return false;
}
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
}
fn has_unterminated_string_or_char(s: &str) -> bool {
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
let c = bytes[i];
if c == b'"' {
if i + 2 < bytes.len() && bytes[i + 1] == b'"' && bytes[i + 2] == b'"' {
let mut j = i + 3;
let mut esc = false;
let mut found = false;
while j < bytes.len() {
if esc {
esc = false;
j += 1;
continue;
}
if bytes[j] == b'\\' {
esc = true;
j += 1;
continue;
}
if j + 2 < bytes.len()
&& bytes[j] == b'"'
&& bytes[j + 1] == b'"'
&& bytes[j + 2] == b'"'
{
found = true;
j += 3;
break;
}
j += 1;
}
if !found {
return true;
}
i = j;
continue;
}
let mut j = i + 1;
let mut esc = false;
let mut found = false;
while j < bytes.len() {
if esc {
esc = false;
j += 1;
continue;
}
if bytes[j] == b'\\' {
esc = true;
j += 1;
continue;
}
if bytes[j] == b'"' {
found = true;
j += 1;
break;
}
j += 1;
}
if !found {
return true;
}
i = j;
continue;
}
if c == b'\'' {
let mut j = i + 1;
let mut esc = false;
let mut found = false;
while j < bytes.len() {
if esc {
esc = false;
j += 1;
continue;
}
if bytes[j] == b'\\' {
esc = true;
j += 1;
continue;
}
if bytes[j] == b'\'' {
found = true;
j += 1;
break;
}
j += 1;
}
if !found {
return true;
}
i = j;
continue;
}
i += 1;
}
false
}
fn contains_bare_arrow(s: &str) -> bool {
let bytes = s.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 c = bytes[i] as char;
if esc {
esc = false;
i += 1;
continue;
}
if in_str {
if c == '\\' {
esc = true;
} else if c == '"' {
in_str = false;
}
i += 1;
continue;
}
if in_char {
if c == '\\' {
esc = true;
} else if c == '\'' {
in_char = false;
}
i += 1;
continue;
}
match c {
'"' => in_str = true,
'\'' => in_char = true,
'-' if i + 2 < bytes.len()
&& bytes[i + 1] == b'>'
&& bytes[i + 2] == b' '
&& i > 0
&& bytes[i - 1] == b' ' =>
{
return true;
}
_ => {}
}
i += 1;
}
false
}
fn looks_like_ordered_list_item(s: &str) -> bool {
let mut chars = s.chars();
let first = match chars.next() {
Some(c) if c.is_ascii_digit() => c,
_ => return false,
};
let _ = first;
let mut rest = &s[1..];
while rest.chars().next().is_some_and(|c| c.is_ascii_digit()) {
rest = &rest[1..];
}
if !rest.starts_with('.') {
return false;
}
let after_dot = &rest[1..];
after_dot.is_empty() || after_dot.starts_with(' ')
}
fn contains_sentence_period(s: &str) -> bool {
let bytes = s.as_bytes();
let mut in_str = false;
let mut in_char = false;
let mut esc = false;
for i in 0..bytes.len() {
let c = bytes[i] as char;
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,
'.' if i + 1 < bytes.len() && bytes[i + 1] == b' ' && i > 0 => {
let prev = bytes[i - 1] as char;
if prev.is_ascii_lowercase() {
return true;
}
}
_ => {}
}
}
false
}
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 block_has_column_aligned_assertions(block_lines: &[&str]) -> bool {
use super::spacing::{
collapse_spaces_outside_strings, space_tight_binary_ops, space_tight_tuples_lists,
};
let mut cols: Vec<usize> = Vec::new();
let mut any_padded = false;
let mut any_incomplete_marker = false;
let mut last_line_incomplete = false;
let mut any_internal_padding = false;
let mut any_compact_syntax = false;
let mut all_canonical_before_op = true;
let mut any_internal_ellipsis = false;
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 {
return false;
}
if trimmed.starts_with("--") {
return false;
}
if !looks_like_assertion(trimmed) {
return false;
}
let Some((col, padded)) = find_top_level_assertion_op(line) else {
return false;
};
let before = &line[leading..col];
let op_and_after = &line[col..];
let canonical = format!("{} {}", before.trim_end(), op_and_after.trim_start());
let c1 = collapse_spaces_outside_strings(&canonical);
let c2 = space_tight_binary_ops(&c1);
let c3 = space_tight_tuples_lists(&c2);
if c3 != canonical {
all_canonical_before_op = false;
}
if has_multi_space_outside_strings(before.trim_end()) {
any_internal_padding = true;
}
if line_has_compact_list_or_tuple(line) {
any_compact_syntax = true;
}
let ends_incomplete = trimmed.ends_with(" ...") || trimmed.ends_with(" ..");
if ends_incomplete {
any_incomplete_marker = true;
}
last_line_incomplete = ends_incomplete;
if has_internal_ellipsis(trimmed) {
any_internal_ellipsis = true;
}
cols.push(col);
if padded {
any_padded = true;
}
}
if cols.len() < 2 {
return false;
}
let first_col = cols[0];
if !cols.iter().all(|&c| c == first_col) {
return false;
}
if !any_padded {
return false;
}
let case_a = all_canonical_before_op && any_incomplete_marker && last_line_incomplete;
let case_b = any_internal_padding && any_compact_syntax;
let case_c = any_internal_ellipsis;
if !(case_a || case_b || case_c) {
return false;
}
true
}
pub(in crate::print) fn has_internal_ellipsis(trimmed: &str) -> bool {
let bytes = trimmed.as_bytes();
if bytes.len() < 4 {
return false;
}
let mut i = 0;
while i + 4 <= bytes.len() {
if bytes[i] == b' ' && bytes[i + 1] == b'.' && bytes[i + 2] == b'.' && bytes[i + 3] == b'.'
{
if i + 4 < bytes.len() && bytes[i + 4] != b'.' {
return true;
}
}
i += 1;
}
false
}
fn line_has_compact_list_or_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'[' {
if let Some(&next) = bytes.get(i + 1)
&& (next == b'"'
|| next == b'\''
|| next == b'('
|| next == b'['
|| next == b'{'
|| next.is_ascii_digit()
|| next.is_ascii_alphabetic()
|| next == b'_')
{
return true;
}
}
if b == b'(' {
if let Some(&next) = bytes.get(i + 1)
&& (next == b'"' || next == b'\'' || next.is_ascii_digit())
{
let mut depth = 1i32;
let mut j = i + 1;
let mut found_comma = false;
let mut inner_str = false;
let mut inner_char = false;
let mut inner_esc = false;
while j < bytes.len() {
let c = bytes[j];
if inner_esc {
inner_esc = false;
j += 1;
continue;
}
if inner_str {
if c == b'\\' {
inner_esc = true;
} else if c == b'"' {
inner_str = false;
}
j += 1;
continue;
}
if inner_char {
if c == b'\\' {
inner_esc = true;
} else if c == b'\'' {
inner_char = false;
}
j += 1;
continue;
}
match c {
b'"' => inner_str = true,
b'\'' => inner_char = true,
b'(' | b'[' | b'{' => depth += 1,
b')' | b']' | b'}' => {
depth -= 1;
if depth == 0 {
break;
}
}
b',' if depth == 1 => {
found_comma = true;
break;
}
_ => {}
}
j += 1;
}
if found_comma {
return true;
}
}
}
i += 1;
}
false
}
fn has_multi_space_outside_strings(s: &str) -> bool {
let bytes = s.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() && bytes[i + 1] == b' ' {
return true;
}
i += 1;
}
false
}
fn find_top_level_assertion_op(line: &str) -> Option<(usize, 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 + 3 < bytes.len() && &bytes[i + 1..i + 4] == b"== " {
let padded = i >= 1 && bytes[i.saturating_sub(1)] == b' ';
return Some((i + 1, padded));
}
if b == b' ' && i + 3 < bytes.len() && &bytes[i + 1..i + 4] == b"-- " {
let padded = i >= 1 && bytes[i.saturating_sub(1)] == b' ';
return Some((i + 1, padded));
}
i += 1;
}
None
}
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().copied() != Some(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 Some(left_first) = left.chars().next() else {
unreachable!("checked !left.is_empty() above")
};
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
}