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:
Nick Cameron
2025-05-06 11:02:55 +12:00
committed by GitHub
parent cf2e9d4b91
commit 574d6dae7f
163 changed files with 1053 additions and 1414 deletions

View File

@ -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;
}