use super::constants::TAB_DISPLAY_WIDTH;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum VisibilityAnnotationForm {
Bare,
Restricted,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct VisibilityAnnotationSite {
pub form: VisibilityAnnotationForm,
pub start: usize,
pub end: usize,
pub end_with_separator: usize,
}
impl VisibilityAnnotationSite {
pub(super) fn locate(source: &str, line: usize, column: usize) -> Option<Self> {
let line_start = line_byte_offset(source, line)?;
let line_end = source[line_start..]
.find('\n')
.map_or(source.len(), |pos| line_start + pos);
let line_text = &source[line_start..line_end];
let column_offset = byte_offset_of_display_column(line_text, column)?;
let annotation_text = &line_text[column_offset..];
let (form, byte_len) = annotation_form_and_byte_len(annotation_text)?;
let start = line_start + column_offset;
let end = start + byte_len;
let separator_len = annotation_text[byte_len..]
.chars()
.take_while(|c| c.is_whitespace())
.map(char::len_utf8)
.sum::<usize>();
Some(Self {
form,
start,
end,
end_with_separator: end + separator_len,
})
}
}
fn annotation_form_and_byte_len(text: &str) -> Option<(VisibilityAnnotationForm, usize)> {
let rest = text.strip_prefix("pub")?;
match rest.chars().next() {
Some('(') => {
let close = rest.find(')')?;
Some((
VisibilityAnnotationForm::Restricted,
"pub".len() + close + 1,
))
},
Some(c) if c.is_whitespace() || c == ':' => {
Some((VisibilityAnnotationForm::Bare, "pub".len()))
},
None => Some((VisibilityAnnotationForm::Bare, "pub".len())),
_ => None,
}
}
fn byte_offset_of_display_column(line_text: &str, column: usize) -> Option<usize> {
let target = column.saturating_sub(1);
let mut display_column = 0;
for (offset, character) in line_text.char_indices() {
if display_column >= target {
return (display_column == target).then_some(offset);
}
display_column += if character == '\t' {
TAB_DISPLAY_WIDTH
} else {
1
};
}
(display_column == target).then_some(line_text.len())
}
fn line_byte_offset(source: &str, line: usize) -> Option<usize> {
if line == 0 {
return None;
}
if line == 1 {
return Some(0);
}
source
.match_indices('\n')
.nth(line - 2)
.map(|(pos, _)| pos + 1)
}
#[cfg(test)]
#[allow(
clippy::expect_used,
reason = "tests should panic on unexpected values"
)]
mod tests {
use super::TAB_DISPLAY_WIDTH;
use super::VisibilityAnnotationForm;
use super::VisibilityAnnotationSite;
use super::annotation_form_and_byte_len;
#[test]
fn bare_pub_is_measured_without_its_separator() {
assert_eq!(
annotation_form_and_byte_len("pub leaked: u32"),
Some((VisibilityAnnotationForm::Bare, 3))
);
}
#[test]
fn pub_crate_is_restricted() {
assert_eq!(
annotation_form_and_byte_len("pub(crate) x"),
Some((VisibilityAnnotationForm::Restricted, "pub(crate)".len()))
);
}
#[test]
fn pub_super_is_restricted() {
assert_eq!(
annotation_form_and_byte_len("pub(super) name"),
Some((VisibilityAnnotationForm::Restricted, "pub(super)".len()))
);
}
#[test]
fn pub_in_path_is_restricted() {
assert_eq!(
annotation_form_and_byte_len("pub(in crate::foo::bar) y"),
Some((
VisibilityAnnotationForm::Restricted,
"pub(in crate::foo::bar)".len()
))
);
}
#[test]
fn text_without_pub_has_no_annotation() {
assert_eq!(annotation_form_and_byte_len("private_field: u32"), None);
}
#[test]
fn unterminated_restricted_annotation_has_no_site() {
let source = "pub(\n in crate::a\n) fn wide() {}\n";
assert_eq!(VisibilityAnnotationSite::locate(source, 1, 1), None);
}
#[test]
fn located_site_covers_annotation_and_separator() {
let source = "mod inner;\n pub fn wide() {}\n";
let site = VisibilityAnnotationSite::locate(source, 2, 5).expect("annotation at 2:5");
assert_eq!(site.form, VisibilityAnnotationForm::Bare);
assert_eq!(&source[site.start..site.end], "pub");
assert_eq!(&source[site.start..site.end_with_separator], "pub ");
}
#[test]
fn tab_indented_annotation_is_located_from_its_display_column() {
let source = "impl Wide {\n\tpub fn wide() {}\n}\n";
let site = VisibilityAnnotationSite::locate(source, 2, TAB_DISPLAY_WIDTH + 1)
.expect("annotation behind one tab");
assert_eq!(site.form, VisibilityAnnotationForm::Bare);
assert_eq!(&source[site.start..site.end], "pub");
assert_eq!(&source[site.start..site.end_with_separator], "pub ");
}
#[test]
fn column_inside_a_tab_has_no_site() {
assert_eq!(
VisibilityAnnotationSite::locate("\tpub fn wide() {}\n", 1, TAB_DISPLAY_WIDTH),
None
);
}
#[test]
fn column_past_the_line_has_no_site() {
assert_eq!(
VisibilityAnnotationSite::locate("pub fn a() {}\n", 1, 99),
None
);
}
}