#![allow(dead_code)]
use std::collections::{BTreeSet, HashMap};
use std::path::Path;
use nova_ast::item::{ExternItem, TraitItem, TypeDef};
use nova_ast::{File, Item, Type, Visibility};
use nova_diagnostics::{FileId, Spanned};
use nova_lexer::Token;
pub const PRIMITIVES: [&str; 7] = ["Int", "Float", "Bool", "Char", "String", "Bytes", "Future"];
pub fn parse(text: &str) -> File {
let (tokens, errors) = nova_lexer::lex(text, FileId::DUMMY);
assert!(errors.is_empty(), "{errors:?}");
let (file, errors) = nova_parser::parse(&tokens, FileId::DUMMY);
assert!(errors.is_empty(), "{errors:?}");
file.expect("the file parses")
}
pub fn module_docs(text: &str) -> String {
let (tokens, comments, _) = nova_lexer::lex_with_comments(text, FileId::DUMMY);
let first = tokens
.iter()
.find(|t| t.value != Token::Eof)
.map_or(text.len(), |t| t.span.start as usize);
comments
.iter()
.filter(|c| c.span.end as usize <= first)
.filter_map(|c| text[c.span.start as usize..c.span.end as usize].strip_prefix("//!"))
.collect::<Vec<_>>()
.join("\n")
}
fn type_name(ty: &Type) -> Option<&str> {
match ty {
Type::Path { path, .. } => path.segments.last().map(|s| s.value.as_str()),
_ => None,
}
}
pub fn public_types(sources: &[Source]) -> BTreeSet<String> {
let mut out: BTreeSet<String> = PRIMITIVES.iter().map(|p| p.to_string()).collect();
for s in sources {
for item in &s.file.items {
match &item.value {
Item::Record(r) if r.vis == Visibility::Pub => {
out.insert(r.name.value.clone());
}
Item::Type(t) if t.vis == Visibility::Pub => {
out.insert(t.name.value.clone());
}
_ => {}
}
}
}
out
}
pub fn needing_docs<'f>(
file: &'f File,
public: &BTreeSet<String>,
) -> Vec<(String, &'f [Spanned<String>])> {
let mut out: Vec<(String, &'f [Spanned<String>])> = Vec::new();
for item in &file.items {
match &item.value {
Item::Function(f) if f.vis == Visibility::Pub => {
out.push((format!("fn {}", f.name.value), &f.docs))
}
Item::Const(c) if c.vis == Visibility::Pub => {
out.push((format!("const {}", c.name.value), &c.docs))
}
Item::Record(r) if r.vis == Visibility::Pub => {
out.push((format!("record {}", r.name.value), &r.docs));
for f in r.fields.iter().filter(|f| f.vis == Visibility::Pub) {
out.push((format!("field {}.{}", r.name.value, f.name.value), &f.docs));
}
}
Item::Type(t) if t.vis == Visibility::Pub => {
out.push((format!("type {}", t.name.value), &t.docs));
if let TypeDef::Sum(variants) = &t.def {
for v in variants {
out.push((
format!("variant {}::{}", t.name.value, v.name.value),
&v.docs,
));
}
}
}
Item::Trait(t) if t.vis == Visibility::Pub => {
out.push((format!("trait {}", t.name.value), &t.docs));
for ti in &t.items {
match ti {
TraitItem::Required(s) => out.push((
format!("method {}::{}", t.name.value, s.name.value),
&s.docs,
)),
TraitItem::Provided(f) => out.push((
format!("method {}::{}", t.name.value, f.name.value),
&f.docs,
)),
TraitItem::AssocType { docs, name, .. } => {
out.push((format!("type {}::{}", t.name.value, name.value), docs))
}
}
}
}
Item::Impl(imp) if imp.trait_.is_none() => {
let Some(on) = type_name(&imp.ty.value) else {
continue;
};
if !public.contains(on) {
continue;
}
for f in imp.functions.iter().filter(|f| f.vis == Visibility::Pub) {
out.push((format!("method {on}.{}", f.name.value), &f.docs));
}
}
_ => {}
}
}
out
}
pub struct Source {
pub page: String,
pub package: String,
pub file: File,
pub all: bool,
}
pub fn std_sources() -> Vec<Source> {
nova_std::ALL
.iter()
.map(|(name, text)| Source {
page: format!("std/{name}/index.html"),
package: "std".to_string(),
file: parse(text),
all: false,
})
.collect()
}
pub fn anchors(sources: &[Source]) -> BTreeSet<(String, String)> {
let mut out = BTreeSet::new();
let mut types: Vec<(&str, &str, &str, &str)> = Vec::new();
for s in sources {
let shown = |v: Visibility| s.all || v == Visibility::Pub;
for item in &s.file.items {
match &item.value {
Item::Function(f) if shown(f.vis) => {
out.insert((s.page.clone(), format!("fn.{}", f.name.value)));
}
Item::Const(c) if shown(c.vis) => {
out.insert((s.page.clone(), format!("const.{}", c.name.value)));
}
Item::Extern(block) if s.all => {
for e in &block.items {
let ExternItem::Fn(sig) = e;
out.insert((s.page.clone(), format!("fn.{}", sig.name.value)));
}
}
Item::Record(r) if shown(r.vis) => {
let a = format!("record.{}", r.name.value);
for f in r.fields.iter().filter(|f| shown(f.vis)) {
out.insert((s.page.clone(), format!("{a}.field.{}", f.name.value)));
}
out.insert((s.page.clone(), a));
types.push((
s.package.as_str(),
r.name.value.as_str(),
s.page.as_str(),
"record",
));
}
Item::Type(t) if shown(t.vis) => {
if let TypeDef::Sum(variants) = &t.def {
let a = format!("type.{}", t.name.value);
for v in variants {
out.insert((s.page.clone(), format!("{a}.variant.{}", v.name.value)));
}
out.insert((s.page.clone(), a));
types.push((
s.package.as_str(),
t.name.value.as_str(),
s.page.as_str(),
"type",
));
}
}
Item::Trait(t) if shown(t.vis) => {
let a = format!("trait.{}", t.name.value);
for ti in &t.items {
let id = match ti {
TraitItem::Required(sig) => format!("{a}.method.{}", sig.name.value),
TraitItem::Provided(f) => format!("{a}.method.{}", f.name.value),
TraitItem::AssocType { name, .. } => format!("{a}.type.{}", name.value),
};
out.insert((s.page.clone(), id));
}
out.insert((s.page.clone(), a));
}
_ => {}
}
}
}
for s in sources {
for item in &s.file.items {
let Item::Impl(imp) = &item.value else {
continue;
};
if imp.trait_.is_some() {
continue;
}
let Some(on) = type_name(&imp.ty.value) else {
continue;
};
for f in imp
.functions
.iter()
.filter(|f| s.all || f.vis == Visibility::Pub)
{
if s.package == "std" && PRIMITIVES.contains(&on) {
out.insert((
format!("std/primitive.{on}.html"),
format!("method.{}", f.name.value),
));
} else if let Some((_, _, page, prefix)) = types
.iter()
.find(|(p, n, _, _)| *p == s.package && *n == on)
{
out.insert((
page.to_string(),
format!("{prefix}.{on}.method.{}", f.name.value),
));
}
}
}
}
out
}
pub fn missing(dir: &Path, expected: &BTreeSet<(String, String)>) -> Vec<String> {
let mut pages: HashMap<&str, String> = HashMap::new();
let mut out = Vec::new();
for (page, id) in expected {
let text = pages
.entry(page.as_str())
.or_insert_with(|| std::fs::read_to_string(dir.join(page)).unwrap_or_default());
if !text.contains(&format!(" id=\"{id}\"")) {
out.push(format!("{page}#{id}"));
}
}
out
}