use dictator_decree_abi::{Diagnostic, Diagnostics, Span};
use memchr::memchr_iter;
use crate::counting::is_comment_only_line;
pub fn check_visibility_ordering(source: &str, diags: &mut Diagnostics) {
let bytes = source.as_bytes();
let mut line_start = 0;
let mut in_struct = false;
let mut in_impl = false;
let mut has_private = false;
let mut in_raw_string = false;
for nl in memchr_iter(b'\n', bytes) {
let line = &source[line_start..nl];
let trimmed = line.trim();
if !in_raw_string && (trimmed.contains("= r\"") || trimmed.contains("= r#\"")) {
let after_open = trimmed.find("= r\"").map_or_else(
|| trimmed.find("= r#\"").map_or("", |pos| &trimmed[pos + 5..]),
|pos| &trimmed[pos + 4..],
);
if !after_open.contains('"') {
in_raw_string = true;
}
} else if in_raw_string && (trimmed.ends_with("\";") || trimmed == "\";" || trimmed == "\"")
{
in_raw_string = false;
line_start = nl + 1;
continue;
}
if in_raw_string {
line_start = nl + 1;
continue;
}
if trimmed.contains("struct ") && trimmed.contains('{') {
in_struct = true;
has_private = false;
} else if trimmed.contains("impl ") && trimmed.contains('{') {
in_impl = true;
has_private = false;
} else if trimmed == "}" || trimmed.starts_with("}\n") {
in_struct = false;
in_impl = false;
has_private = false;
}
if (in_struct || in_impl) && !trimmed.is_empty() && !is_comment_only_line(trimmed) {
let is_pub = trimmed.starts_with("pub ");
let is_field_or_method = is_struct_field_or_impl_item(trimmed);
if is_field_or_method {
if !is_pub && !has_private {
has_private = true;
} else if is_pub && has_private {
diags.push(Diagnostic {
rule: "rust/visibility-order".to_string(),
message:
"Public item found after private item. Expected all public items first"
.to_string(),
enforced: false,
span: Span::new(line_start, nl),
});
}
}
}
line_start = nl + 1;
}
}
fn is_struct_field_or_impl_item(trimmed: &str) -> bool {
if trimmed.is_empty()
|| trimmed == "}"
|| trimmed.starts_with('}')
|| trimmed.starts_with('#')
|| trimmed.starts_with("//")
{
return false;
}
if trimmed.starts_with("fn ")
|| trimmed.starts_with("pub fn ")
|| trimmed.starts_with("const ")
|| trimmed.starts_with("pub const ")
|| trimmed.starts_with("type ")
|| trimmed.starts_with("pub type ")
|| trimmed.starts_with("unsafe fn ")
|| trimmed.starts_with("pub unsafe fn ")
|| trimmed.starts_with("async fn ")
|| trimmed.starts_with("pub async fn ")
{
return true;
}
let field_part = trimmed.strip_prefix("pub ").unwrap_or(trimmed);
field_part.find(':').is_some_and(|colon_pos| {
let before_colon = field_part[..colon_pos].trim();
!before_colon.is_empty()
&& before_colon
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
&& before_colon
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_')
})
}
#[cfg(test)]
mod tests;