use std::collections::BTreeMap;
use std::fmt::Display;
use std::fmt::Formatter;
use std::fmt::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Class {
Auto,
Review,
Manual,
}
#[allow(unused_imports)]
use Class::*;
impl Display for Class {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
match self {
Class::Auto => write!(f, "auto"),
Class::Review => write!(f, "review"),
Class::Manual => write!(f, "manual"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Position {
Anywhere,
AtEnd,
AtStart,
}
#[allow(unused_imports)]
use Position::*;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Marker {
pub needle: String,
pub construct: String,
pub kind: Class,
pub position: Position,
}
pub fn markers() -> Vec<Marker> {
let mut found = Vec::new();
at_end(&mut found, "do", "a `do` block", Class::Manual);
add(
&mut found,
"Option<Option<",
"a nested optional",
Class::Manual,
);
add(
&mut found,
"FieldNames",
"`FieldNames` in a derive template",
Class::Manual,
);
add(
&mut found,
"Fields",
"`Fields` in a derive template",
Class::Manual,
);
add(
&mut found,
"__vlt_",
"a generated identifier named by hand",
Class::Manual,
);
add(
&mut found,
".and_then",
"`.and_then` over a std shape",
Class::Review,
);
add(&mut found, "where", "a `where` group", Class::Review);
add(&mut found, "|>", "the `|>` pipeline", Class::Auto);
add(&mut found, ">>>", "forward composition", Class::Auto);
add(&mut found, "<<<", "reverse composition", Class::Auto);
add(&mut found, "otherwise", "`otherwise`", Class::Auto);
add(&mut found, "newtype ", "`newtype`", Class::Auto);
add(&mut found, "errors ", "`errors`", Class::Auto);
add(&mut found, "deriving ", "`deriving`", Class::Auto);
add(&mut found, "instance ", "an `instance` header", Class::Auto);
add(&mut found, "import ", "`import`", Class::Auto);
add(&mut found, "export ", "`export`", Class::Auto);
add(&mut found, "module ", "`module`", Class::Auto);
add(&mut found, "async ", "`async`", Class::Auto);
add(&mut found, ".await", "`await`", Class::Auto);
add(&mut found, "catch ", "`catch`", Class::Auto);
add(&mut found, "&mut ", "an explicit `&mut`", Class::Auto);
at_start(&mut found, "if", "an `if` statement", Class::Review);
at_start(&mut found, "while", "a `while` loop", Class::Review);
at_start(&mut found, "for", "a `for` loop", Class::Review);
add(&mut found, "case", "a `case` expression", Class::Review);
at_start(&mut found, "guard", "a `guard`", Class::Auto);
found
}
fn add(into: &mut Vec<Marker>, needle: &str, construct: &str, kind: Class) {
at(into, needle, construct, kind, Position::Anywhere);
}
fn at_end(into: &mut Vec<Marker>, needle: &str, construct: &str, kind: Class) {
at(into, needle, construct, kind, Position::AtEnd);
}
fn at_start(into: &mut Vec<Marker>, needle: &str, construct: &str, kind: Class) {
at(into, needle, construct, kind, Position::AtStart);
}
fn at(into: &mut Vec<Marker>, needle: &str, construct: &str, kind: Class, position: Position) {
into.push(Marker {
needle: needle.to_string(),
construct: construct.to_string(),
kind,
position,
});
();
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Finding {
pub line: usize,
pub construct: String,
pub kind: Class,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Verdict {
pub kind: Class,
pub findings: Vec<Finding>,
}
impl Verdict {
pub fn of_class(&self, wanted: Class) -> Vec<&Finding> {
self.findings.iter().filter(|f| f.kind == wanted).collect()
}
}
pub fn classify(source: &str) -> Verdict {
let table = markers();
let mut findings = Vec::new();
let mut worst = Class::Auto;
for pair in source.lines().enumerate() {
let text = pair.1;
if is_comment(text) {
continue;
}
for marker in table.iter() {
if hits(text, marker) {
if marker.kind > worst {
worst = marker.kind;
}
findings.push(Finding {
line: pair.0 + 1,
construct: marker.construct.clone(),
kind: marker.kind,
});
}
}
}
Verdict {
kind: worst,
findings,
}
}
pub fn hits(text: &str, marker: &Marker) -> bool {
let needle = marker.needle.as_str();
match marker.position {
Position::AtEnd => text.trim_end().ends_with(needle) && mentions(text, needle),
Position::AtStart => opens_a_statement(text, needle),
Position::Anywhere => mentions(text, needle),
}
}
pub fn opens_a_statement(text: &str, raw: &str) -> bool {
let needle = raw.trim();
let body = text.trim_start();
if !body.starts_with(needle) {
return false;
}
let start = needle.len();
let end = body.len();
start >= end || !is_word_byte(&body[start..start + 1])
}
pub fn mentions(text: &str, raw: &str) -> bool {
if !is_word(raw) {
return text.contains(raw);
}
let needle = raw.trim();
for hit in text.match_indices(needle) {
let start = hit.0;
let end = start + needle.len();
let before_ok = start == 0 || !is_word_byte(&text[start - 1..start]);
let after_ok = end >= text.len() || !is_word_byte(&text[end..end + 1]);
if before_ok && after_ok {
return true;
}
}
false
}
fn is_word(needle: &str) -> bool {
needle
.trim()
.chars()
.all(|c| c == '_' || char::is_alphanumeric(c) || c == ' ')
}
fn is_word_byte(held: &str) -> bool {
held.chars().any(|c| c == '_' || char::is_alphanumeric(c))
}
fn is_comment(text: &str) -> bool {
text.trim_start().starts_with("--")
}
#[derive(Debug, Clone, Default)]
pub struct Tally {
pub files: usize,
pub by_class: BTreeMap<Class, usize>,
}
impl Tally {
pub fn new() -> Self {
Tally {
files: 0,
by_class: BTreeMap::new(),
}
}
pub fn record(&mut self, kind: Class) {
self.files += 1;
let counter = self.by_class.entry(kind).or_insert(0);
*counter += 1;
();
}
pub fn count(&self, kind: Class) -> usize {
self.by_class.get(&kind).copied().unwrap_or(0)
}
pub fn blocking(&self) -> usize {
self.count(Class::Manual)
}
}
#[cfg(test)]
mod __vlt_tests {
use super::*;
#[test]
fn a_tail_needle_must_end_the_line_because_do_introduces_a_block() {
assert_eq!(classify("held = do\n").kind, Class::Manual);
assert_eq!(
classify("test \"two names do not conflict\"\n").kind,
Class::Auto,
"the English word is not the construct"
);
assert_eq!(
classify(" \"do\" -> Some Do\n").kind,
Class::Auto,
"a keyword table names `do` without using it"
);
}
#[test]
fn a_tail_needle_still_needs_its_word_boundary() {
assert_eq!(
classify("shown = tornado\n").kind,
Class::Auto,
"`tornado` ends in `do` and is not a block"
);
}
#[test]
fn a_word_needle_needs_a_word_boundary_on_both_sides() {
assert!(mentions("data Fields = Fields", "Fields"));
assert!(
!mentions("ConstructorFields::Record", "Fields"),
"a longer identifier is not the construct"
);
assert!(
!mentions("elsewhere = 1", "where"),
"`elsewhere` is not a `where` group"
);
}
#[test]
fn a_punctuation_needle_cannot_have_a_word_boundary_so_it_matches_as_a_substring() {
assert!(mentions("shown = value |> .to_string", "|>"));
assert!(mentions("held.await", ".await"));
}
#[test]
fn a_needle_at_the_very_start_or_end_of_a_line_still_matches() {
assert!(mentions("Fields", "Fields"));
assert!(mentions("newtype Meters = Meters f64", "newtype "));
}
#[test]
fn the_classifier_does_not_match_its_own_marker_table() {
let found = classify(" at_end (&mut found) \"do\" \"a block\" Class::Manual\n");
assert_eq!(
found.kind,
Class::Auto,
"a source file that describes source is the hardest case for text matching, and the tail anchor is what answers it"
);
}
#[test]
fn a_file_with_nothing_notable_is_auto() {
let found = classify("twice : u64 -> u64\ntwice n = n + n\n");
assert_eq!(found.kind, Class::Auto);
}
#[test]
fn a_pipeline_is_auto_because_the_rewrite_is_mechanical() {
let found = classify("shown = value |> .to_string\n");
assert_eq!(found.kind, Class::Auto);
assert_eq!(found.of_class(Class::Auto).len(), 1);
}
#[test]
fn a_nested_optional_is_manual_and_doc_86_5_6_is_why() {
let found = classify("held : Option<Option<u64>>\n");
assert_eq!(found.kind, Class::Manual);
}
#[test]
fn the_worst_class_in_a_file_is_the_file_s_class() {
let found = classify("shown = value |> .to_string\nheld : Option<Option<u64>>\n");
assert_eq!(
found.kind,
Class::Manual,
"one manual construct makes the file manual"
);
assert_eq!(
found.of_class(Class::Auto).len(),
1,
"the auto finding is still reported"
);
}
#[test]
fn review_sits_between_auto_and_manual() {
assert!(Class::Auto < Class::Review);
assert!(Class::Review < Class::Manual);
}
#[test]
fn a_construct_inside_a_comment_is_not_a_finding() {
let found = classify(
"-- this names |> and Option<Option<u64>> and neither counts\ntwice : u64 -> u64\ntwice n = n + n\n",
);
assert_eq!(found.kind, Class::Auto, "a comment is prose, not code");
assert!(found.findings.is_empty());
}
#[test]
fn a_finding_carries_its_line_counting_from_one() {
let found = classify("twice : u64 -> u64\ntwice n = n |> .count\n");
assert_eq!(found.findings[0].line, 2);
}
#[test]
fn a_tally_counts_files_per_class_and_names_the_blocking_number() {
let mut tally = Tally::new();
tally.record(Class::Auto);
tally.record(Class::Auto);
tally.record(Class::Manual);
assert_eq!(tally.files, 3);
assert_eq!(tally.count(Class::Auto), 2);
assert_eq!(tally.count(Class::Review), 0);
assert_eq!(
tally.blocking(),
1,
"doc 89 milestone 17 asks this to reach zero"
);
}
#[test]
fn every_marker_names_a_construct_so_a_report_can_be_read() {
for marker in markers().iter() {
assert!(!marker.needle.is_empty());
assert!(!marker.construct.is_empty());
}
}
#[test]
fn the_marker_table_covers_all_three_classes() {
let mut seen = Vec::new();
for marker in markers().iter() {
if !seen.contains(&marker.kind) {
seen.push(marker.kind);
}
}
assert_eq!(
seen.len(),
3,
"a table missing a class would report that class as absent"
);
}
}