Add modules to docs (#6699)
* Support modules in docs Signed-off-by: Nick Cameron <nrc@ncameron.org> * shuffle around directories Signed-off-by: Nick Cameron <nrc@ncameron.org> --------- Signed-off-by: Nick Cameron <nrc@ncameron.org>
This commit is contained in:
@ -9,23 +9,14 @@ use tower_lsp::lsp_types::{
|
||||
use crate::{
|
||||
execution::annotations,
|
||||
parsing::{
|
||||
ast::types::{Annotation, ImportSelector, ItemVisibility, Node, VariableKind},
|
||||
ast::types::{Annotation, ImportSelector, ItemVisibility, Node, NonCodeValue, VariableKind},
|
||||
token::NumericSuffix,
|
||||
},
|
||||
ModuleId,
|
||||
};
|
||||
|
||||
pub fn walk_prelude() -> Vec<DocData> {
|
||||
let mut visitor = CollectionVisitor::default();
|
||||
visitor.visit_module("prelude", "", WalkForNames::All).unwrap();
|
||||
visitor.result.into_values().collect()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct CollectionVisitor {
|
||||
name: String,
|
||||
result: HashMap<String, DocData>,
|
||||
id: usize,
|
||||
pub fn walk_prelude() -> ModData {
|
||||
visit_module("prelude", "", WalkForNames::All).unwrap()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@ -50,93 +41,122 @@ impl<'a> WalkForNames<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl CollectionVisitor {
|
||||
fn visit_module(&mut self, name: &str, preferred_prefix: &str, names: WalkForNames) -> Result<(), String> {
|
||||
let old_name = std::mem::replace(&mut self.name, name.to_owned());
|
||||
let source = crate::modules::read_std(name).unwrap();
|
||||
let parsed = crate::parsing::parse_str(source, ModuleId::from_usize(self.id))
|
||||
.parse_errs_as_err()
|
||||
.unwrap();
|
||||
self.id += 1;
|
||||
fn visit_module(name: &str, preferred_prefix: &str, names: WalkForNames) -> Result<ModData, String> {
|
||||
let mut result = ModData::new(name, preferred_prefix);
|
||||
|
||||
for n in &parsed.body {
|
||||
if n.visibility() != ItemVisibility::Export {
|
||||
continue;
|
||||
let source = crate::modules::read_std(name).unwrap();
|
||||
let parsed = crate::parsing::parse_str(source, ModuleId::from_usize(0))
|
||||
.parse_errs_as_err()
|
||||
.unwrap();
|
||||
|
||||
// TODO handle examples; use with_comments
|
||||
let mut summary = String::new();
|
||||
let mut description = None;
|
||||
for n in &parsed.non_code_meta.start_nodes {
|
||||
match &n.value {
|
||||
NonCodeValue::BlockComment { value, .. } if value.starts_with('/') => {
|
||||
let line = value[1..].trim();
|
||||
if line.is_empty() {
|
||||
match &mut description {
|
||||
None => description = Some(String::new()),
|
||||
Some(d) => d.push_str("\n\n"),
|
||||
}
|
||||
} else {
|
||||
match &mut description {
|
||||
None => {
|
||||
summary.push_str(line);
|
||||
summary.push(' ');
|
||||
}
|
||||
Some(d) => {
|
||||
d.push_str(line);
|
||||
d.push(' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
match n {
|
||||
crate::parsing::ast::types::BodyItem::ImportStatement(import) => match &import.path {
|
||||
crate::parsing::ast::types::ImportPath::Std { path } => match &import.selector {
|
||||
ImportSelector::Glob(..) => self.visit_module(&path[1], "", names.clone())?,
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
if !summary.is_empty() {
|
||||
result.summary = Some(summary);
|
||||
}
|
||||
result.description = description;
|
||||
|
||||
for n in &parsed.body {
|
||||
if n.visibility() != ItemVisibility::Export {
|
||||
continue;
|
||||
}
|
||||
match n {
|
||||
crate::parsing::ast::types::BodyItem::ImportStatement(import) => match &import.path {
|
||||
crate::parsing::ast::types::ImportPath::Std { path } => {
|
||||
let m = match &import.selector {
|
||||
ImportSelector::Glob(..) => Some(visit_module(&path[1], "", names.clone())?),
|
||||
ImportSelector::None { .. } => {
|
||||
let name = import.module_name().unwrap();
|
||||
if names.contains(&name) {
|
||||
self.visit_module(&path[1], &format!("{}::", name), WalkForNames::All)?;
|
||||
Some(visit_module(&path[1], &format!("{}::", name), WalkForNames::All)?)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
ImportSelector::List { items } => {
|
||||
self.visit_module(&path[1], "", names.intersect(items.iter().map(|n| &*n.name.name)))?
|
||||
}
|
||||
},
|
||||
p => return Err(format!("Unexpected import: `{p}`")),
|
||||
},
|
||||
crate::parsing::ast::types::BodyItem::VariableDeclaration(var) => {
|
||||
if !names.contains(var.name()) {
|
||||
continue;
|
||||
}
|
||||
let qual_name = if self.name == "prelude" {
|
||||
"std::".to_owned()
|
||||
} else {
|
||||
format!("std::{}::", self.name)
|
||||
ImportSelector::List { items } => Some(visit_module(
|
||||
&path[1],
|
||||
"",
|
||||
names.intersect(items.iter().map(|n| &*n.name.name)),
|
||||
)?),
|
||||
};
|
||||
let mut dd = match var.kind {
|
||||
VariableKind::Fn => DocData::Fn(FnData::from_ast(var, qual_name, preferred_prefix, name)),
|
||||
VariableKind::Const => {
|
||||
DocData::Const(ConstData::from_ast(var, qual_name, preferred_prefix, name))
|
||||
}
|
||||
};
|
||||
let key = format!("I:{}", dd.qual_name());
|
||||
if self.result.contains_key(&key) {
|
||||
continue;
|
||||
if let Some(m) = m {
|
||||
result.children.insert(format!("M:{}", m.qual_name), DocData::Mod(m));
|
||||
}
|
||||
|
||||
dd.with_meta(&var.outer_attrs);
|
||||
for a in &var.outer_attrs {
|
||||
dd.with_comments(&a.pre_comments);
|
||||
}
|
||||
dd.with_comments(n.get_comments());
|
||||
|
||||
self.result.insert(key, dd);
|
||||
}
|
||||
crate::parsing::ast::types::BodyItem::TypeDeclaration(ty) => {
|
||||
if !names.contains(ty.name()) {
|
||||
continue;
|
||||
}
|
||||
let qual_name = if self.name == "prelude" {
|
||||
"std::".to_owned()
|
||||
} else {
|
||||
format!("std::{}::", self.name)
|
||||
};
|
||||
let mut dd = DocData::Ty(TyData::from_ast(ty, qual_name, preferred_prefix, name));
|
||||
let key = format!("T:{}", dd.qual_name());
|
||||
if self.result.contains_key(&key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
dd.with_meta(&ty.outer_attrs);
|
||||
for a in &ty.outer_attrs {
|
||||
dd.with_comments(&a.pre_comments);
|
||||
}
|
||||
dd.with_comments(n.get_comments());
|
||||
|
||||
self.result.insert(key, dd);
|
||||
p => return Err(format!("Unexpected import: `{p}`")),
|
||||
},
|
||||
crate::parsing::ast::types::BodyItem::VariableDeclaration(var) => {
|
||||
if !names.contains(var.name()) {
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
let qual = format!("{}::", &result.qual_name);
|
||||
let mut dd = match var.kind {
|
||||
VariableKind::Fn => DocData::Fn(FnData::from_ast(var, qual, preferred_prefix, name)),
|
||||
VariableKind::Const => DocData::Const(ConstData::from_ast(var, qual, preferred_prefix, name)),
|
||||
};
|
||||
let key = format!("I:{}", dd.qual_name());
|
||||
if result.children.contains_key(&key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
dd.with_meta(&var.outer_attrs);
|
||||
for a in &var.outer_attrs {
|
||||
dd.with_comments(&a.pre_comments);
|
||||
}
|
||||
dd.with_comments(n.get_comments());
|
||||
|
||||
result.children.insert(key, dd);
|
||||
}
|
||||
}
|
||||
crate::parsing::ast::types::BodyItem::TypeDeclaration(ty) => {
|
||||
if !names.contains(ty.name()) {
|
||||
continue;
|
||||
}
|
||||
let qual = format!("{}::", &result.qual_name);
|
||||
let mut dd = DocData::Ty(TyData::from_ast(ty, qual, preferred_prefix, name));
|
||||
let key = format!("T:{}", dd.qual_name());
|
||||
if result.children.contains_key(&key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
self.name = old_name;
|
||||
Ok(())
|
||||
dd.with_meta(&ty.outer_attrs);
|
||||
for a in &ty.outer_attrs {
|
||||
dd.with_comments(&a.pre_comments);
|
||||
}
|
||||
dd.with_comments(n.get_comments());
|
||||
|
||||
result.children.insert(key, dd);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -144,6 +164,7 @@ pub enum DocData {
|
||||
Fn(FnData),
|
||||
Const(ConstData),
|
||||
Ty(TyData),
|
||||
Mod(ModData),
|
||||
}
|
||||
|
||||
impl DocData {
|
||||
@ -152,6 +173,7 @@ impl DocData {
|
||||
DocData::Fn(f) => &f.name,
|
||||
DocData::Const(c) => &c.name,
|
||||
DocData::Ty(t) => &t.name,
|
||||
DocData::Mod(m) => &m.name,
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,6 +183,7 @@ impl DocData {
|
||||
DocData::Fn(f) => &f.preferred_name,
|
||||
DocData::Const(c) => &c.preferred_name,
|
||||
DocData::Ty(t) => &t.preferred_name,
|
||||
DocData::Mod(m) => &m.preferred_name,
|
||||
}
|
||||
}
|
||||
|
||||
@ -169,6 +192,7 @@ impl DocData {
|
||||
DocData::Fn(f) => &f.qual_name,
|
||||
DocData::Const(c) => &c.qual_name,
|
||||
DocData::Ty(t) => &t.qual_name,
|
||||
DocData::Mod(m) => &m.qual_name,
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,6 +203,7 @@ impl DocData {
|
||||
DocData::Fn(f) => &f.module_name,
|
||||
DocData::Const(c) => &c.module_name,
|
||||
DocData::Ty(t) => &t.module_name,
|
||||
DocData::Mod(m) => &m.module_name,
|
||||
}
|
||||
}
|
||||
|
||||
@ -188,6 +213,7 @@ impl DocData {
|
||||
DocData::Fn(f) => format!("functions/{}", f.qual_name.replace("::", "-")),
|
||||
DocData::Const(c) => format!("consts/{}", c.qual_name.replace("::", "-")),
|
||||
DocData::Ty(t) => format!("types/{}", t.qual_name.replace("::", "-")),
|
||||
DocData::Mod(m) => format!("modules/{}", m.qual_name.replace("::", "-")),
|
||||
}
|
||||
}
|
||||
|
||||
@ -197,6 +223,7 @@ impl DocData {
|
||||
DocData::Fn(f) => format!("fn_{}", f.qual_name.replace("::", "-")),
|
||||
DocData::Const(c) => format!("const_{}", c.qual_name.replace("::", "-")),
|
||||
DocData::Ty(t) => format!("ty_{}", t.qual_name.replace("::", "-")),
|
||||
DocData::Mod(_) => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -212,6 +239,7 @@ impl DocData {
|
||||
}
|
||||
&t.qual_name
|
||||
}
|
||||
DocData::Mod(m) => &m.qual_name,
|
||||
};
|
||||
q[0..q.rfind("::").unwrap()].to_owned()
|
||||
}
|
||||
@ -222,14 +250,16 @@ impl DocData {
|
||||
DocData::Fn(f) => f.properties.doc_hidden || f.properties.deprecated,
|
||||
DocData::Const(c) => c.properties.doc_hidden || c.properties.deprecated,
|
||||
DocData::Ty(t) => t.properties.doc_hidden || t.properties.deprecated,
|
||||
DocData::Mod(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_completion_item(&self) -> CompletionItem {
|
||||
pub fn to_completion_item(&self) -> Option<CompletionItem> {
|
||||
match self {
|
||||
DocData::Fn(f) => f.to_completion_item(),
|
||||
DocData::Const(c) => c.to_completion_item(),
|
||||
DocData::Ty(t) => t.to_completion_item(),
|
||||
DocData::Fn(f) => Some(f.to_completion_item()),
|
||||
DocData::Const(c) => Some(c.to_completion_item()),
|
||||
DocData::Ty(t) => Some(t.to_completion_item()),
|
||||
DocData::Mod(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -238,6 +268,7 @@ impl DocData {
|
||||
DocData::Fn(f) => Some(f.to_signature_help()),
|
||||
DocData::Const(_) => None,
|
||||
DocData::Ty(_) => None,
|
||||
DocData::Mod(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -246,6 +277,7 @@ impl DocData {
|
||||
DocData::Fn(f) => f.with_meta(attrs),
|
||||
DocData::Const(c) => c.with_meta(attrs),
|
||||
DocData::Ty(t) => t.with_meta(attrs),
|
||||
DocData::Mod(m) => m.with_meta(attrs),
|
||||
}
|
||||
}
|
||||
|
||||
@ -254,6 +286,7 @@ impl DocData {
|
||||
DocData::Fn(f) => f.with_comments(comments),
|
||||
DocData::Const(c) => c.with_comments(comments),
|
||||
DocData::Ty(t) => t.with_comments(comments),
|
||||
DocData::Mod(m) => m.with_comments(comments),
|
||||
}
|
||||
}
|
||||
|
||||
@ -263,9 +296,17 @@ impl DocData {
|
||||
DocData::Fn(f) => f.examples.iter(),
|
||||
DocData::Const(c) => c.examples.iter(),
|
||||
DocData::Ty(t) => t.examples.iter(),
|
||||
DocData::Mod(_) => unimplemented!(),
|
||||
}
|
||||
.filter_map(|(s, p)| (!p.norun).then_some(s))
|
||||
}
|
||||
|
||||
fn expect_mod(&self) -> &ModData {
|
||||
match self {
|
||||
DocData::Mod(m) => m,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -384,6 +425,70 @@ impl ConstData {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ModData {
|
||||
pub name: String,
|
||||
/// How the module is indexed, etc.
|
||||
pub preferred_name: String,
|
||||
/// The fully qualified name.
|
||||
pub qual_name: String,
|
||||
/// The summary of the module.
|
||||
pub summary: Option<String>,
|
||||
/// The description of the module.
|
||||
pub description: Option<String>,
|
||||
pub module_name: String,
|
||||
|
||||
pub children: HashMap<String, DocData>,
|
||||
}
|
||||
|
||||
impl ModData {
|
||||
fn new(name: &str, preferred_prefix: &str) -> Self {
|
||||
let (qual_name, module_name) = if name == "prelude" {
|
||||
("std".to_owned(), String::new())
|
||||
} else {
|
||||
(format!("std::{}", name), "std".to_owned())
|
||||
};
|
||||
Self {
|
||||
preferred_name: format!("{preferred_prefix}{name}"),
|
||||
name: name.to_owned(),
|
||||
qual_name,
|
||||
summary: None,
|
||||
description: None,
|
||||
children: HashMap::new(),
|
||||
module_name,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn find_by_name(&self, name: &str) -> Option<&DocData> {
|
||||
if let Some(result) = self.children.values().find(|dd| dd.name() == name) {
|
||||
return Some(result);
|
||||
}
|
||||
|
||||
#[allow(clippy::iter_over_hash_type)]
|
||||
for (k, v) in &self.children {
|
||||
if k.starts_with("M:") {
|
||||
if let Some(result) = v.expect_mod().find_by_name(name) {
|
||||
return Some(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn all_docs(&self) -> impl Iterator<Item = &DocData> {
|
||||
let result = self.children.values();
|
||||
// TODO really this should be recursive, currently assume std is only one module deep.
|
||||
result.chain(
|
||||
self.children
|
||||
.iter()
|
||||
.filter(|(k, _)| k.starts_with("M:"))
|
||||
.flat_map(|(_, d)| d.expect_mod().children.values()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FnData {
|
||||
/// The name of the function.
|
||||
@ -985,6 +1090,29 @@ impl ApplyMeta for FnData {
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplyMeta for ModData {
|
||||
fn apply_docs(
|
||||
&mut self,
|
||||
summary: Option<String>,
|
||||
description: Option<String>,
|
||||
examples: Vec<(String, ExampleProperties)>,
|
||||
) {
|
||||
self.summary = summary;
|
||||
self.description = description;
|
||||
assert!(examples.is_empty());
|
||||
}
|
||||
|
||||
fn deprecated(&mut self, deprecated: bool) {
|
||||
assert!(!deprecated);
|
||||
}
|
||||
|
||||
fn doc_hidden(&mut self, doc_hidden: bool) {
|
||||
assert!(!doc_hidden);
|
||||
}
|
||||
|
||||
fn impl_kind(&mut self, _: annotations::Impl) {}
|
||||
}
|
||||
|
||||
impl ApplyMeta for TyData {
|
||||
fn apply_docs(
|
||||
&mut self,
|
||||
@ -1050,16 +1178,14 @@ mod test {
|
||||
#[test]
|
||||
fn smoke() {
|
||||
let result = walk_prelude();
|
||||
for d in result {
|
||||
if let DocData::Const(d) = d {
|
||||
if d.name == "PI" {
|
||||
assert!(d.value.unwrap().starts_with('3'));
|
||||
assert_eq!(d.ty, Some("number(_?)".to_owned()));
|
||||
assert_eq!(d.qual_name, "std::math::PI");
|
||||
assert!(d.summary.is_some());
|
||||
assert!(!d.examples.is_empty());
|
||||
return;
|
||||
}
|
||||
if let DocData::Const(d) = result.find_by_name("PI").unwrap() {
|
||||
if d.name == "PI" {
|
||||
assert!(d.value.as_ref().unwrap().starts_with('3'));
|
||||
assert_eq!(d.ty, Some("number(_?)".to_owned()));
|
||||
assert_eq!(d.qual_name, "std::math::PI");
|
||||
assert!(d.summary.is_some());
|
||||
assert!(!d.examples.is_empty());
|
||||
return;
|
||||
}
|
||||
}
|
||||
panic!("didn't find PI");
|
||||
@ -1088,8 +1214,19 @@ mod test {
|
||||
async fn kcl_test_examples() {
|
||||
let std = walk_prelude();
|
||||
let mut errs = Vec::new();
|
||||
for d in std {
|
||||
if d.module_name() != STD_MOD_NAME {
|
||||
|
||||
let data = if STD_MOD_NAME == "prelude" {
|
||||
&std
|
||||
} else {
|
||||
std.children
|
||||
.get(&format!("M:std::{STD_MOD_NAME}"))
|
||||
.unwrap()
|
||||
.expect_mod()
|
||||
};
|
||||
|
||||
#[allow(clippy::iter_over_hash_type)]
|
||||
for d in data.children.values() {
|
||||
if let DocData::Mod(_) = d {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user