Don't render undexposed types in docs

This commit is contained in:
Richard Feldman 2022-12-01 21:26:06 -05:00
parent 15590fb31b
commit 15d4c2ad68
No known key found for this signature in database
GPG key ID: F1F21AA5B1D9E43B
4 changed files with 286 additions and 170 deletions

View file

@ -149,6 +149,7 @@ pub struct ModuleOutput {
pub rigid_variables: RigidVariables,
pub declarations: Declarations,
pub exposed_imports: MutMap<Symbol, Region>,
pub exposed_symbols: VecSet<Symbol>,
pub problems: Vec<Problem>,
pub referenced_values: VecSet<Symbol>,
pub referenced_types: VecSet<Symbol>,
@ -280,7 +281,7 @@ pub fn canonicalize_module_defs<'a>(
aliases: MutMap<Symbol, Alias>,
imported_abilities_state: PendingAbilitiesStore,
exposed_imports: MutMap<Ident, (Symbol, Region)>,
exposed_symbols: &VecSet<Symbol>,
exposed_symbols: VecSet<Symbol>,
symbols_from_requires: &[(Loc<Symbol>, Loc<TypeAnnotation<'a>>)],
var_store: &mut VarStore,
) -> ModuleOutput {
@ -810,6 +811,7 @@ pub fn canonicalize_module_defs<'a>(
pending_derives,
loc_expects: collected.expects,
loc_dbgs: collected.dbgs,
exposed_symbols,
}
}

View file

@ -1,9 +1,9 @@
use crate::docs::DocEntry::DetachedDoc;
use crate::docs::TypeAnnotation::{Apply, BoundVariable, Function, NoTypeAnn, Record, TagUnion};
use roc_can::scope::Scope;
use roc_collections::MutMap;
use roc_collections::VecSet;
use roc_module::ident::ModuleName;
use roc_module::symbol::{IdentIds, ModuleId};
use roc_module::symbol::{IdentIds, ModuleId, ModuleIds, Symbol};
use roc_parse::ast::AssignedField;
use roc_parse::ast::{self, ExtractSpaces, TypeHeader};
use roc_parse::ast::{CommentOrNewline, TypeDef, ValueDef};
@ -15,6 +15,7 @@ pub struct ModuleDocumentation {
pub name: String,
pub entries: Vec<DocEntry>,
pub scope: Scope,
pub exposed_symbols: VecSet<Symbol>,
}
#[derive(Debug, Clone)]
@ -26,6 +27,7 @@ pub enum DocEntry {
#[derive(Debug, Clone)]
pub struct DocDef {
pub name: String,
pub symbol: Symbol,
pub type_vars: Vec<String>,
pub type_annotation: TypeAnnotation,
pub docs: Option<String>,
@ -90,16 +92,26 @@ pub struct Tag {
pub fn generate_module_docs(
scope: Scope,
home: ModuleId,
module_ids: &ModuleIds,
module_name: ModuleName,
parsed_defs: &roc_parse::ast::Defs,
exposed_module_ids: &[ModuleId],
exposed_symbols: VecSet<Symbol>,
) -> ModuleDocumentation {
let entries = generate_entry_docs(&scope.locals.ident_ids, parsed_defs, exposed_module_ids);
let entries = generate_entry_docs(
home,
&scope.locals.ident_ids,
module_ids,
parsed_defs,
exposed_module_ids,
);
ModuleDocumentation {
name: module_name.as_str().to_string(),
scope,
entries,
exposed_symbols,
}
}
@ -130,9 +142,11 @@ fn detached_docs_from_comments_and_new_lines<'a>(
detached_docs
}
fn generate_entry_docs<'a>(
ident_ids: &'a IdentIds,
defs: &roc_parse::ast::Defs<'a>,
fn generate_entry_docs(
home: ModuleId,
ident_ids: &IdentIds,
module_ids: &ModuleIds,
defs: &roc_parse::ast::Defs<'_>,
exposed_module_ids: &[ModuleId],
) -> Vec<DocEntry> {
use roc_parse::ast::Pattern;
@ -160,10 +174,11 @@ fn generate_entry_docs<'a>(
ValueDef::Annotation(loc_pattern, loc_ann) => {
if let Pattern::Identifier(identifier) = loc_pattern.value {
// Check if this module exposes the def
if ident_ids.get_id(identifier).is_some() {
if let Some(ident_id) = ident_ids.get_id(identifier) {
let name = identifier.to_string();
let doc_def = DocDef {
name,
symbol: Symbol::new(home, ident_id),
type_annotation: type_to_docs(false, loc_ann.value),
type_vars: Vec::new(),
docs,
@ -180,11 +195,12 @@ fn generate_entry_docs<'a>(
} => {
if let Pattern::Identifier(identifier) = ann_pattern.value {
// Check if the definition is exposed
if ident_ids.get_id(identifier).is_some() {
if let Some(ident_id) = ident_ids.get_id(identifier) {
let doc_def = DocDef {
name: identifier.to_string(),
type_annotation: type_to_docs(false, ann_type.value),
type_vars: Vec::new(),
symbol: Symbol::new(home, ident_id),
docs,
};
acc.push(DocEntry::DocDef(doc_def));
@ -220,11 +236,20 @@ fn generate_entry_docs<'a>(
}
}
let type_annotation =
if contains_unexposed_type(&ann.value, exposed_module_ids, module_ids) {
TypeAnnotation::NoTypeAnn
} else {
type_to_docs(false, ann.value)
};
let ident_id = ident_ids.get_id(name.value).unwrap();
let doc_def = DocDef {
name: name.value.to_string(),
type_annotation: type_to_docs(false, ann.value),
type_annotation,
type_vars,
docs,
symbol: Symbol::new(home, ident_id),
};
acc.push(DocEntry::DocDef(doc_def));
}
@ -241,11 +266,13 @@ fn generate_entry_docs<'a>(
}
}
let ident_id = ident_ids.get_id(name.value).unwrap();
let doc_def = DocDef {
name: name.value.to_string(),
type_annotation: TypeAnnotation::NoTypeAnn,
type_vars,
docs,
symbol: Symbol::new(home, ident_id),
};
acc.push(DocEntry::DocDef(doc_def));
}
@ -279,9 +306,11 @@ fn generate_entry_docs<'a>(
})
.collect();
let ident_id = ident_ids.get_id(name.value).unwrap();
let doc_def = DocDef {
name: name.value.to_string(),
type_annotation: TypeAnnotation::Ability { members },
symbol: Symbol::new(home, ident_id),
type_vars,
docs,
};
@ -303,6 +332,121 @@ fn generate_entry_docs<'a>(
acc
}
fn contains_unexposed_type(
ann: &ast::TypeAnnotation,
exposed_module_ids: &[ModuleId],
module_ids: &ModuleIds,
) -> bool {
use ast::TypeAnnotation::*;
match ann {
// Apply is the one case that can directly return true.
Apply(module_name, _ident, loc_args) => {
// If the *ident* was unexposed, we would have gotten a naming error
// during canonicalization, so all we need to check is the module.
let module_id = module_ids.get_id(&(*module_name).into()).unwrap();
!exposed_module_ids.contains(&module_id)
|| loc_args.iter().any(|loc_arg| {
contains_unexposed_type(&loc_arg.value, exposed_module_ids, module_ids)
})
}
Malformed(_) | Inferred | Wildcard | BoundVariable(_) => false,
Function(loc_args, loc_ret) => {
contains_unexposed_type(&loc_ret.value, exposed_module_ids, module_ids)
|| loc_args.iter().any(|loc_arg| {
contains_unexposed_type(&loc_arg.value, exposed_module_ids, module_ids)
})
}
Record { fields, ext } => {
if let Some(loc_ext) = ext {
if contains_unexposed_type(&loc_ext.value, exposed_module_ids, module_ids) {
return true;
}
}
let mut fields_to_process =
Vec::from_iter(fields.iter().map(|loc_field| loc_field.value));
while let Some(field) = fields_to_process.pop() {
match field {
AssignedField::RequiredValue(_field, _spaces, loc_val)
| AssignedField::OptionalValue(_field, _spaces, loc_val) => {
if contains_unexposed_type(&loc_val.value, exposed_module_ids, module_ids) {
return true;
}
}
AssignedField::Malformed(_) | AssignedField::LabelOnly(_) => {
// contains no unexposed types, so continue
}
AssignedField::SpaceBefore(field, _) | AssignedField::SpaceAfter(field, _) => {
fields_to_process.push(*field);
}
}
}
false
}
Tuple { fields, ext } => {
if let Some(loc_ext) = ext {
if contains_unexposed_type(&loc_ext.value, exposed_module_ids, module_ids) {
return true;
}
}
fields.iter().any(|loc_field| {
contains_unexposed_type(&loc_field.value, exposed_module_ids, module_ids)
})
}
TagUnion { ext, tags } => {
use ast::Tag;
if let Some(loc_ext) = ext {
if contains_unexposed_type(&loc_ext.value, exposed_module_ids, module_ids) {
return true;
}
}
let mut tags_to_process = Vec::from_iter(tags.iter().map(|loc_tag| loc_tag.value));
while let Some(tag) = tags_to_process.pop() {
match tag {
Tag::Apply { name: _, args } => {
for loc_ann in args.iter() {
if contains_unexposed_type(
&loc_ann.value,
exposed_module_ids,
module_ids,
) {
return true;
}
}
}
Tag::Malformed(_) => {
// contains no unexposed types, so continue
}
Tag::SpaceBefore(tag, _) | Tag::SpaceAfter(tag, _) => {
tags_to_process.push(*tag);
}
}
}
false
}
Where(loc_ann, _loc_has_clauses) => {
// We assume all the abilities in the `has` clause are from exported modules.
// TODO don't assume this! Instead, look them up and verify.
contains_unexposed_type(&loc_ann.value, exposed_module_ids, module_ids)
}
As(loc_ann, _spaces, _type_header) => {
contains_unexposed_type(&loc_ann.value, exposed_module_ids, module_ids)
}
SpaceBefore(ann, _) | ast::TypeAnnotation::SpaceAfter(ann, _) => {
contains_unexposed_type(ann, exposed_module_ids, module_ids)
}
}
}
fn type_to_docs(in_func_type_ann: bool, type_annotation: ast::TypeAnnotation) -> TypeAnnotation {
match type_annotation {
ast::TypeAnnotation::TagUnion { tags, ext } => {

View file

@ -636,7 +636,7 @@ pub struct LoadedModule {
pub resolved_implementations: ResolvedImplementations,
pub sources: MutMap<ModuleId, (PathBuf, Box<str>)>,
pub timings: MutMap<ModuleId, ModuleTiming>,
pub documentation: MutMap<ModuleId, ModuleDocumentation>,
pub docs_by_module: MutMap<ModuleId, ModuleDocumentation>,
pub abilities_store: AbilitiesStore,
}
@ -3429,7 +3429,7 @@ fn finish(
resolved_implementations,
sources,
timings: state.timings,
documentation,
docs_by_module: documentation,
abilities_store,
}
}
@ -5251,7 +5251,7 @@ fn canonicalize_and_constrain<'a>(
aliases,
imported_abilities_state,
exposed_imports,
&exposed_symbols,
exposed_symbols,
&symbols_from_requires,
&mut var_store,
);
@ -5289,9 +5289,12 @@ fn canonicalize_and_constrain<'a>(
scope.add_docs_imports();
let docs = crate::docs::generate_module_docs(
scope,
module_id,
module_ids,
name.as_str().into(),
&parsed_defs_for_docs,
exposed_module_ids,
module_output.exposed_symbols.clone(),
);
Some(docs)
@ -5364,7 +5367,7 @@ fn canonicalize_and_constrain<'a>(
let module = Module {
module_id,
exposed_imports: module_output.exposed_imports,
exposed_symbols,
exposed_symbols: module_output.exposed_symbols,
referenced_values: module_output.referenced_values,
referenced_types: module_output.referenced_types,
aliases,

View file

@ -8,15 +8,17 @@ use html::mark_node_to_html;
use roc_can::scope::Scope;
use roc_code_markup::markup::nodes::MarkupNode;
use roc_code_markup::slow_pool::SlowPool;
use roc_collections::{default_hasher, MutMap, VecSet};
use roc_highlight::highlight_parser::{highlight_defs, highlight_expr};
use roc_load::docs::{DocEntry, TypeAnnotation};
use roc_load::docs::{ModuleDocumentation, RecordField};
use roc_load::{ExecutionMode, LoadConfig, LoadedModule, LoadingProblem, Threading};
use roc_module::symbol::{IdentIdsByModule, Interns, ModuleId};
use roc_module::symbol::{IdentIdsByModule, Interns, ModuleId, Symbol};
use roc_packaging::cache::{self, RocCacheDir};
use roc_parse::ident::{parse_ident, Ident};
use roc_parse::state::State;
use roc_region::all::Region;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
@ -59,42 +61,41 @@ pub fn generate_docs_html(root_file: PathBuf) {
)
.expect("TODO gracefully handle failing to make the favicon");
let module_pairs = loaded_module
.documentation
.iter()
.flat_map(|(module_id, module)| {
let exposed_values = loaded_module
.exposed_values
.iter()
.map(|symbol| symbol.as_str(&loaded_module.interns).to_string())
.collect::<Vec<String>>();
Some((module, exposed_values))
});
let template_html = include_str!("./static/index.html")
.replace("<!-- search.js -->", "/search.js")
.replace("<!-- styles.css -->", "/styles.css")
.replace("<!-- favicon.svg -->", "/favicon.svg")
.replace(
"<!-- Prefetch links -->",
&module_pairs
.clone()
.map(|(module, _)| {
loaded_module
.docs_by_module
.iter()
.map(|(_, module)| {
let href = sidebar_link_url(module.name.as_str());
format!(r#"<link rel="prefetch" href="{href}"/>"#)
})
.collect::<Vec<String>>()
.join("\n "),
.join("\n ")
.as_str(),
)
.replace(
"<!-- Module links -->",
render_sidebar(module_pairs).as_str(),
render_sidebar(loaded_module.docs_by_module.values()).as_str(),
);
let all_exposed_symbols = {
let mut set = VecSet::default();
for docs in loaded_module.docs_by_module.values() {
set.insert_all(docs.exposed_symbols.iter().copied());
}
set
};
// Write each package's module docs html file
for (module_id, module_docs) in loaded_module.documentation.iter() {
for (module_id, module_docs) in loaded_module.docs_by_module.iter() {
let module_name = module_docs.name.as_str();
let module_dir = build_dir.join(module_name.replace('.', "/").as_str());
@ -112,7 +113,13 @@ pub fn generate_docs_html(root_file: PathBuf) {
)
.replace(
"<!-- Module Docs -->",
render_module_documentation(module_docs, &loaded_module).as_str(),
render_module_documentation(
*module_id,
module_docs,
&loaded_module,
&all_exposed_symbols,
)
.as_str(),
);
fs::write(module_dir.join("index.html"), rendered_module)
@ -174,41 +181,38 @@ pub fn syntax_highlight_top_level_defs(code_str: &str) -> DocsResult<String> {
}
fn render_module_documentation(
home: ModuleId,
module: &ModuleDocumentation,
loaded_module: &LoadedModule,
root_module: &LoadedModule,
all_exposed_symbols: &VecSet<Symbol>,
) -> String {
let mut buf = String::new();
buf.push_str(
html_to_string(
"h2",
vec![("class", "module-name")],
html_to_string("a", vec![("href", "/#")], module.name.as_str()).as_str(),
)
.as_str(),
push_html(&mut buf, "h2", vec![("class", "module-name")], {
let mut link_buf = String::new();
push_html(
&mut link_buf,
"a",
vec![("href", "/#")],
module.name.as_str(),
);
let exposed_values = loaded_module.exposed_values_str();
let exposed_aliases = loaded_module.exposed_aliases_str();
link_buf
});
for entry in &module.entries {
match entry {
DocEntry::DocDef(doc_def) => {
let name_str = doc_def.name.as_str();
// We dont want to render entries that arent exposed
let should_render_entry =
exposed_values.contains(&name_str) || exposed_aliases.contains(&name_str);
if should_render_entry {
// Only redner entries that are exposed
if all_exposed_symbols.contains(&doc_def.symbol) {
buf.push_str("<section>");
let name = doc_def.name.as_str();
let href = format!("#{name}");
let mut content = String::new();
content.push_str(
html_to_string("a", vec![("href", href.as_str())], name).as_str(),
);
push_html(&mut content, "a", vec![("href", href.as_str())], name);
for type_var in &doc_def.type_vars {
content.push(' ');
@ -222,24 +226,21 @@ fn render_module_documentation(
type_annotation_to_html(0, &mut content, type_ann, false);
}
buf.push_str(
html_to_string(
push_html(
&mut buf,
"h3",
vec![("id", name), ("class", "entry-name")],
content.as_str(),
)
.as_str(),
);
if let Some(docs) = &doc_def.docs {
buf.push_str(
markdown_to_html(
&exposed_values,
&mut buf,
home,
all_exposed_symbols,
&module.scope,
docs.to_string(),
loaded_module,
)
.as_str(),
docs,
root_module,
);
}
@ -247,13 +248,14 @@ fn render_module_documentation(
}
}
DocEntry::DetachedDoc(docs) => {
let markdown = markdown_to_html(
&exposed_values,
markdown_to_html(
&mut buf,
home,
&all_exposed_symbols,
&module.scope,
docs.to_string(),
loaded_module,
docs,
root_module,
);
buf.push_str(markdown.as_str());
}
};
}
@ -261,9 +263,7 @@ fn render_module_documentation(
buf
}
fn html_to_string(tag_name: &str, attrs: Vec<(&str, &str)>, content: &str) -> String {
let mut buf = String::new();
fn push_html(buf: &mut String, tag_name: &str, attrs: Vec<(&str, &str)>, content: impl AsRef<str>) {
buf.push('<');
buf.push_str(tag_name);
@ -281,13 +281,11 @@ fn html_to_string(tag_name: &str, attrs: Vec<(&str, &str)>, content: &str) -> St
buf.push('>');
buf.push_str(content);
buf.push_str(content.as_ref());
buf.push_str("</");
buf.push_str(tag_name);
buf.push('>');
buf
}
fn base_url() -> String {
@ -326,14 +324,13 @@ fn render_name_and_version(name: &str, version: &str) -> String {
url_str.push_str(name);
buf.push_str(
html_to_string(
"h1",
vec![("class", "pkg-full-name")],
html_to_string("a", vec![("href", url_str.as_str())], name).as_str(),
)
.as_str(),
);
push_html(&mut buf, "h1", vec![("class", "pkg-full-name")], {
let mut link_buf = String::new();
push_html(&mut link_buf, "a", vec![("href", url_str.as_str())], name);
link_buf
});
let mut versions_url_str = base_url();
@ -342,34 +339,28 @@ fn render_name_and_version(name: &str, version: &str) -> String {
versions_url_str.push('/');
versions_url_str.push_str(version);
buf.push_str(
html_to_string(
push_html(
&mut buf,
"a",
vec![("class", "version"), ("href", versions_url_str.as_str())],
version,
)
.as_str(),
);
buf
}
fn render_sidebar<'a, I: Iterator<Item = (&'a ModuleDocumentation, Vec<String>)>>(
modules: I,
) -> String {
fn render_sidebar<'a, I: Iterator<Item = &'a ModuleDocumentation>>(modules: I) -> String {
let mut buf = String::new();
for (module, exposed_values) in modules {
for module in modules {
let href = sidebar_link_url(module.name.as_str());
let mut sidebar_entry_content = String::new();
sidebar_entry_content.push_str(
html_to_string(
push_html(
&mut sidebar_entry_content,
"a",
vec![("class", "sidebar-module-link"), ("href", &href)],
module.name.as_str(),
)
.as_str(),
);
let entries = {
@ -377,20 +368,18 @@ fn render_sidebar<'a, I: Iterator<Item = (&'a ModuleDocumentation, Vec<String>)>
for entry in &module.entries {
if let DocEntry::DocDef(doc_def) = entry {
if exposed_values.contains(&doc_def.name) {
if module.exposed_symbols.contains(&doc_def.symbol) {
let mut entry_href = String::new();
entry_href.push_str(href.as_str());
entry_href.push('#');
entry_href.push_str(doc_def.name.as_str());
entries_buf.push_str(
html_to_string(
push_html(
&mut entries_buf,
"a",
vec![("href", entry_href.as_str())],
doc_def.name.as_str(),
)
.as_str(),
);
}
}
@ -399,22 +388,18 @@ fn render_sidebar<'a, I: Iterator<Item = (&'a ModuleDocumentation, Vec<String>)>
entries_buf
};
sidebar_entry_content.push_str(
html_to_string(
push_html(
&mut sidebar_entry_content,
"div",
vec![("class", "sidebar-sub-entries")],
entries.as_str(),
)
.as_str(),
);
buf.push_str(
html_to_string(
push_html(
&mut buf,
"div",
vec![("class", "sidebar-entry")],
sidebar_entry_content.as_str(),
)
.as_str(),
);
}
@ -736,7 +721,7 @@ struct DocUrl {
fn doc_url<'a>(
home: ModuleId,
exposed_values: &[&str],
all_exposed_symbols: &VecSet<Symbol>,
dep_idents: &IdentIdsByModule,
scope: &Scope,
interns: &'a Interns,
@ -765,39 +750,23 @@ fn doc_url<'a>(
} else {
match interns.module_ids.get_id(&module_name.into()) {
Some(module_id) => {
// You can do qualified lookups on your own module, e.g.
let symbol = interns.symbol(module_id, ident.into());
// Note: You can do qualified lookups on your own module, e.g.
// if I'm in the Foo module, I can do a `Foo.bar` lookup.
if module_id == home {
// Check to see if the value is exposed in this module.
// If it's not exposed, then we can't link to it!
if !exposed_values.contains(&ident) {
if !all_exposed_symbols.contains(&symbol) {
// TODO return Err here
panic!(
"Tried to generate an automatic link in docs for `{}.{}`, but `{}` does not expose `{}`.",
module_name, ident, module_name, ident);
}
} else {
// This is not the home module
match dep_idents
.get(&module_id)
.and_then(|exposed_ids| exposed_ids.get_id(ident))
{
Some(_) => {
// This is a valid symbol for this dependency,
// so proceed using the current module's name.
//
// TODO: In the future, this is where we'll
// incorporate the package name into the link.
}
_ => {
// TODO return Err here
panic!(
"Tried to generate an automatic link in docs for `{}.{}`, but `{}` is not exposed in `{}`.",
module_name, ident, ident, module_name);
}
}
}
}
None => {
// TODO return Err here
panic!("Tried to generate a doc link for `{}.{}` but the `{}` module was not imported!", module_name, ident, module_name);
@ -821,11 +790,13 @@ fn doc_url<'a>(
}
fn markdown_to_html(
exposed_values: &[&str],
buf: &mut String,
home: ModuleId,
all_exposed_symbols: &VecSet<Symbol>,
scope: &Scope,
markdown: String,
markdown: &str,
loaded_module: &LoadedModule,
) -> String {
) {
use pulldown_cmark::{BrokenLink, CodeBlockKind, CowStr, Event, LinkType, Tag::*};
let mut arena = Bump::new();
@ -851,8 +822,8 @@ fn markdown_to_html(
match iter.next() {
Some(symbol_name) if iter.next().is_none() => {
let DocUrl { url, title } = doc_url(
loaded_module.module_id,
exposed_values,
home,
all_exposed_symbols,
&loaded_module.dep_idents,
scope,
&loaded_module.interns,
@ -874,8 +845,8 @@ fn markdown_to_html(
// This looks like a tag name, but it could
// be a type alias that's in scope, e.g. [I64]
let DocUrl { url, title } = doc_url(
loaded_module.module_id,
exposed_values,
home,
all_exposed_symbols,
&loaded_module.dep_idents,
scope,
&loaded_module.interns,
@ -1002,9 +973,5 @@ fn markdown_to_html(
}
});
let mut docs_html = String::new();
pulldown_cmark::html::push_html(&mut docs_html, docs_parser.into_iter());
docs_html
pulldown_cmark::html::push_html(buf, docs_parser.into_iter());
}