diff --git a/docs/luneTypes.d.luau b/docs/luneTypes.d.luau index 9d23776..b37b6c1 100644 --- a/docs/luneTypes.d.luau +++ b/docs/luneTypes.d.luau @@ -305,7 +305,7 @@ declare process: { cwd: string, --[=[ @within process - @new_fields + @read_write Current environment variables for this process. diff --git a/packages/cli/src/gen/doc2/item.rs b/packages/cli/src/gen/doc2/item.rs index df64cb1..dc9c014 100644 --- a/packages/cli/src/gen/doc2/item.rs +++ b/packages/cli/src/gen/doc2/item.rs @@ -71,6 +71,10 @@ impl DocItem { self.kind.is_tag() } + pub fn kind(&self) -> DocItemKind { + self.kind + } + pub fn get_name(&self) -> Option<&str> { self.name.as_deref() } @@ -82,12 +86,8 @@ impl DocItem { pub fn get_value(&self) -> Option<&str> { self.value.as_deref() } -} -impl IntoIterator for DocItem { - type Item = DocItem; - type IntoIter = std::vec::IntoIter; - fn into_iter(self) -> Self::IntoIter { - self.children.into_iter() + pub fn children(&self) -> &[DocItem] { + &self.children } } diff --git a/packages/cli/src/gen/doc2/parser.rs b/packages/cli/src/gen/doc2/parser.rs index 5f6cd85..f082e80 100644 --- a/packages/cli/src/gen/doc2/parser.rs +++ b/packages/cli/src/gen/doc2/parser.rs @@ -10,6 +10,7 @@ use regex::Regex; use super::{builder::DocItemBuilder, item::DocItem, kind::DocItemKind}; +#[derive(Debug, Clone)] struct DocVisitorItem { name: String, comment: Option, @@ -20,9 +21,9 @@ struct DocVisitorItem { impl From for DocItem { fn from(value: DocVisitorItem) -> Self { let mut builder = DocItemBuilder::new() - .with_kind(match value.type_info { + .with_kind(match &value.type_info { TypeInfo::Array { .. } | TypeInfo::Table { .. } => DocItemKind::Table, - TypeInfo::Callback { .. } => DocItemKind::Function, + typ if type_info_is_fn(typ) => DocItemKind::Function, _ => unimplemented!("Support for globals that are not properties or functions is not yet implemented") }) .with_name(value.name); @@ -39,7 +40,7 @@ impl From for DocItem { builder = builder.with_child( DocItemBuilder::new() .with_kind(match field.value() { - TypeInfo::Callback { .. } => DocItemKind::Function, + typ if type_info_is_fn(typ) => DocItemKind::Function, _ => DocItemKind::Property, }) .with_name(name.token().to_string()) @@ -84,6 +85,18 @@ where .collect()) } +fn type_info_is_fn(typ: &TypeInfo) -> bool { + match typ { + TypeInfo::Callback { .. } => true, + TypeInfo::Tuple { types, .. } => types.iter().all(type_info_is_fn), + TypeInfo::Union { left, right, .. } => type_info_is_fn(left) && type_info_is_fn(right), + TypeInfo::Intersection { left, right, .. } => { + type_info_is_fn(left) && type_info_is_fn(right) + } + _ => false, + } +} + fn should_separate_tag_meta(tag_kind: &str) -> bool { matches!(tag_kind.trim().to_ascii_lowercase().as_ref(), "param") } diff --git a/packages/cli/src/gen/doc2/tag.rs b/packages/cli/src/gen/doc2/tag.rs index 741ab48..d3c1aec 100644 --- a/packages/cli/src/gen/doc2/tag.rs +++ b/packages/cli/src/gen/doc2/tag.rs @@ -12,12 +12,43 @@ pub enum DocsItemTag { Return(String), MustUse, ReadOnly, - NewFields, + ReadWrite, } -impl TryFrom for DocsItemTag { +#[allow(dead_code)] +impl DocsItemTag { + pub fn is_class(&self) -> bool { + matches!(self, Self::Class(_)) + } + + pub fn is_within(&self) -> bool { + matches!(self, Self::Within(_)) + } + + pub fn is_param(&self) -> bool { + matches!(self, Self::Param(_)) + } + + pub fn is_return(&self) -> bool { + matches!(self, Self::Return(_)) + } + + pub fn is_must_use(&self) -> bool { + self == &Self::MustUse + } + + pub fn is_read_only(&self) -> bool { + self == &Self::ReadOnly + } + + pub fn is_read_write(&self) -> bool { + self == &Self::ReadWrite + } +} + +impl TryFrom<&DocItem> for DocsItemTag { type Error = anyhow::Error; - fn try_from(value: DocItem) -> Result { + fn try_from(value: &DocItem) -> Result { if let Some(name) = value.get_name() { Ok(match name.trim().to_ascii_lowercase().as_ref() { "class" => Self::Class( @@ -50,7 +81,7 @@ impl TryFrom for DocsItemTag { ), "must_use" => Self::MustUse, "read_only" => Self::ReadOnly, - "new_fields" => Self::NewFields, + "read_write" => Self::ReadWrite, s => bail!("Unknown docs tag: '{}'", s), }) } else { diff --git a/packages/cli/src/gen/doc2/tree.rs b/packages/cli/src/gen/doc2/tree.rs index 982f6cf..7c90d0c 100644 --- a/packages/cli/src/gen/doc2/tree.rs +++ b/packages/cli/src/gen/doc2/tree.rs @@ -46,11 +46,3 @@ impl DerefMut for DocTree { &mut self.0 } } - -impl IntoIterator for DocTree { - type Item = DocItem; - type IntoIter = std::vec::IntoIter; - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} diff --git a/packages/cli/src/gen/selene_defs.rs b/packages/cli/src/gen/selene_defs.rs index 6a62383..32d846d 100644 --- a/packages/cli/src/gen/selene_defs.rs +++ b/packages/cli/src/gen/selene_defs.rs @@ -1,9 +1,119 @@ use anyhow::{Context, Result}; -use serde_yaml::Value as YamlValue; +use serde_yaml::{Mapping as YamlMapping, Sequence as YamlSequence, Value as YamlValue}; -use super::doc::DocumentationVisitor; +use crate::gen::doc2::DocsItemTag; + +use super::doc2::{DocItem, DocItemKind, DocTree}; pub fn generate_from_type_definitions(contents: &str) -> Result { - let _visitor = DocumentationVisitor::from_definitions(contents)?; - serde_yaml::to_string(&YamlValue::Null).context("Failed to encode docs as json") + let tree = DocTree::from_type_definitions(contents)?; + let mut globals = YamlMapping::new(); + let top_level_items = tree.children().iter().filter(|top_level| { + top_level.is_function() + || top_level.children().iter().any(|top_level_child| { + top_level_child.is_tag() && top_level_child.get_name().unwrap() == "class" + }) + }); + for top_level_item in top_level_items { + match top_level_item.kind() { + DocItemKind::Table => { + let top_level_name = top_level_item + .get_name() + .context("Missing name for top-level doc item")? + .to_string(); + for child_item in top_level_item + .children() + .iter() + .filter(|item| item.is_function() || item.is_property()) + { + let child_name = child_item + .get_name() + .context("Missing name for top-level child doc item")? + .to_string(); + globals.insert( + YamlValue::String(format!("{top_level_name}.{child_name}")), + YamlValue::Mapping(doc_item_to_selene_yaml_mapping(child_item)?), + ); + } + } + DocItemKind::Function => { + globals.insert( + YamlValue::String( + top_level_item + .get_name() + .context("Missing name for top-level doc item")? + .to_string(), + ), + YamlValue::Mapping(doc_item_to_selene_yaml_mapping(top_level_item)?), + ); + } + _ => unimplemented!("Globals other than tables and functions are not yet implemented"), + } + } + let mut contents = YamlMapping::new(); + contents.insert( + YamlValue::String("globals".to_string()), + YamlValue::Mapping(globals), + ); + Ok(format!( + "# Lune v{}\n---\n{}", + env!("CARGO_PKG_VERSION"), + serde_yaml::to_string(&contents).context("Failed to encode type definitions as yaml")? + )) +} + +fn doc_item_to_selene_yaml_mapping(item: &DocItem) -> Result { + let mut mapping = YamlMapping::new(); + if item.is_property() { + let property_access_tag = item + .children() + .iter() + .find_map(|child| { + if let Ok(tag) = DocsItemTag::try_from(child) { + if tag.is_read_only() || tag.is_read_write() { + Some(tag) + } else { + None + } + } else { + None + } + }) + .with_context(|| { + format!( + "Missing property access tag for doc item:\n{}", + item.get_name().unwrap() + ) + })?; + mapping.insert( + YamlValue::String("property".to_string()), + YamlValue::String( + match property_access_tag { + DocsItemTag::ReadOnly => "read-only", + DocsItemTag::ReadWrite => "new-fields", + _ => unreachable!(), + } + .to_string(), + ), + ); + } else if item.is_function() { + let is_must_use = item.children().iter().any(|child| { + if let Ok(tag) = DocsItemTag::try_from(child) { + tag.is_must_use() + } else { + false + } + }); + if is_must_use { + mapping.insert( + YamlValue::String("must_use".to_string()), + YamlValue::Bool(true), + ); + } + mapping.insert( + YamlValue::String("args".to_string()), + YamlValue::Mapping(YamlMapping::new()), + ); + } + Ok(mapping) }