use super::lex::{skeleton, text_of};
use super::scan::{args_of, chain_of, rust_files, sites};
use super::{CONTROLS, HOVERS};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
type Memo = HashMap<String, Vec<String>>;
const HOPS: usize = 3;
const WIDTH: usize = 100;
#[test]
fn every_control_hover_names_its_keyboard_spelling() {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let sources: Vec<(PathBuf, String)> = rust_files(&root)
.into_iter()
.map(|file| {
let skeleton = skeleton(&std::fs::read_to_string(&file).unwrap());
(file, skeleton)
})
.collect();
let tree: String = sources
.iter()
.map(|(_, s)| s.as_str())
.collect::<Vec<_>>()
.join(" ");
let vocabulary = vocabulary();
let (mut mute, mut seen, mut memo) = (Vec::new(), 0usize, Memo::new());
for (file, skeleton) in &sources {
for (at, control) in sites(skeleton, CONTROLS) {
let hovers: Vec<usize> = chain_of(skeleton, at)
.into_iter()
.filter(|(name, _)| HOVERS.contains(&name.as_str()))
.filter_map(|(_, called)| called)
.collect();
if hovers.is_empty() {
continue;
}
seen += 1;
let said: String = hovers
.into_iter()
.map(|open| says(skeleton, &tree, &mut memo, args_of(skeleton, open)))
.collect();
if !vocabulary.iter().any(|spelling| said.contains(spelling)) {
let words: String = said.chars().take(WIDTH).collect();
mute.push(format!("{}: {control} — said {words}", file.display()));
}
}
}
assert!(
seen > 50,
"the scan matched {seen} hovers — the pattern list has rotted"
);
assert!(
mute.is_empty(),
"these controls hide their keyboard spelling — §11 rule 3 requires every \
hover to name the key that presses it, or the §8.5 line that addresses \
it:\n{}",
mute.join("\n")
);
}
fn vocabulary() -> Vec<String> {
let mut words = crate::keymap::spell::spellings();
words.push(crate::keymap::spell::FLOOR.to_owned());
words.extend(
crate::boundary::help::table()
.iter()
.map(|row| format!("/{}", row.verb)),
);
words
}
fn says(file: &str, tree: &str, memo: &mut Memo, arg: &str) -> String {
let (mut words, mut spans, mut asked) = (String::new(), vec![text_of(arg)], Vec::new());
for _ in 0..HOPS {
let mut next = Vec::new();
for span in spans {
let said = span.contains('"');
words.push_str("ed(&span));
for ident in idents(&span) {
let named = ident == ident.to_uppercase();
if (named || !said) && !asked.contains(&ident) {
next.extend(homes(file, tree, memo, &ident));
asked.push(ident);
}
}
}
spans = next;
}
words
}
fn homes(file: &str, tree: &str, memo: &mut Memo, ident: &str) -> Vec<String> {
let own = defined_in(file, ident);
if !own.is_empty() {
return own;
}
memo.entry(ident.to_owned())
.or_insert_with(|| defined_in(tree, ident))
.clone()
}
fn defined_in(skeleton: &str, ident: &str) -> Vec<String> {
let mut found = Vec::new();
for (keyword, closes) in [("const ", ";"), ("let ", ";"), ("fn ", "fn ")] {
let needle = format!("{keyword}{ident}");
let mut from = 0;
while let Some(hit) = skeleton.get(from..).and_then(|rest| rest.find(&needle)) {
from = from + hit + needle.len();
let rest = skeleton.get(from..).unwrap_or_default();
if rest.starts_with(|c: char| c.is_alphanumeric() || c == '_') {
continue;
}
let end = rest.find(closes).unwrap_or(rest.len());
found.push(text_of(rest.get(..end).unwrap_or_default()));
}
}
found
}
fn quoted(span: &str) -> String {
span.split('"')
.skip(1)
.step_by(2)
.collect::<Vec<_>>()
.join(" ")
}
fn idents(span: &str) -> Vec<String> {
span.split(|c: char| !(c.is_alphanumeric() || c == '_'))
.filter(|word| word.len() > 1)
.map(str::to_owned)
.collect()
}