diff --git a/src/adapter.rs b/src/adapter.rs index 2545895..9b3d19a 100644 --- a/src/adapter.rs +++ b/src/adapter.rs @@ -1,4 +1,4 @@ -use std::{collections::BTreeSet, sync::Arc}; +use std::{collections::BTreeSet, rc::Rc, sync::Arc}; use rustdoc_types::{ Crate, Enum, Function, Id, Impl, Item, ItemEnum, Path, Span, Struct, Trait, Type, Variant, @@ -9,6 +9,7 @@ use trustfall_core::{ schema::Schema, }; +use crate::attributes::{Attribute, AttributeMetaItem}; use crate::indexed_crate::IndexedCrate; #[non_exhaustive] @@ -76,13 +77,23 @@ impl Origin { } } - fn make_attribute_token<'a>(&self, attr: &'a str) -> Token<'a> { + fn make_attribute_token<'a>(&self, attr: Attribute<'a>) -> Token<'a> { Token { origin: *self, kind: TokenKind::Attribute(attr), } } + fn make_attribute_meta_item_token<'a>( + &self, + meta_item: Rc>, + ) -> Token<'a> { + Token { + origin: *self, + kind: TokenKind::AttributeMetaItem(meta_item), + } + } + fn make_implemented_trait_token<'a>( &self, path: &'a rustdoc_types::Path, @@ -119,7 +130,8 @@ pub enum TokenKind<'a> { Path(&'a [String]), ImportablePath(Vec<&'a str>), RawType(&'a Type), - Attribute(&'a str), + Attribute(Attribute<'a>), + AttributeMetaItem(Rc>), ImplementedTrait(&'a Path, &'a Item), FunctionParameter(&'a str), } @@ -156,6 +168,7 @@ impl<'a> Token<'a> { TokenKind::Crate(..) => "Crate", TokenKind::CrateDiff(..) => "CrateDiff", TokenKind::Attribute(..) => "Attribute", + TokenKind::AttributeMetaItem(..) => "AttributeMetaItem", TokenKind::ImplementedTrait(..) => "ImplementedTrait", TokenKind::RawType(ty) => match ty { rustdoc_types::Type::ResolvedPath { .. } => "ResolvedPathType", @@ -268,9 +281,16 @@ impl<'a> Token<'a> { }) } - fn as_attribute(&self) -> Option<&'a str> { + fn as_attribute(&self) -> Option<&'_ Attribute<'a>> { match &self.kind { - TokenKind::Attribute(attr) => Some(*attr), + TokenKind::Attribute(attr) => Some(attr), + _ => None, + } + } + + fn as_attribute_meta_item(&self) -> Option<&'_ AttributeMetaItem<'a>> { + match &self.kind { + TokenKind::AttributeMetaItem(meta_item) => Some(meta_item), _ => None, } } @@ -438,9 +458,22 @@ fn get_impl_property(token: &Token, field_name: &str) -> FieldValue { } fn get_attribute_property(token: &Token, field_name: &str) -> FieldValue { - let attribute_token = token.as_attribute().expect("token was not an Attribute"); + let attribute = token.as_attribute().expect("token was not an Attribute"); match field_name { - "value" => attribute_token.into(), + "raw_attribute" => attribute.raw_attribute().into(), + "is_inner" => attribute.is_inner.into(), + _ => unreachable!("Attribute property {field_name}"), + } +} + +fn get_attribute_meta_item_property(token: &Token, field_name: &str) -> FieldValue { + let meta_item = token + .as_attribute_meta_item() + .expect("token was not an AttributeMetaItem"); + match field_name { + "raw_item" => meta_item.raw_item.into(), + "base" => meta_item.base.into(), + "assigned_item" => meta_item.assigned_item.into(), _ => unreachable!("Attribute property {field_name}"), } } @@ -584,6 +617,9 @@ impl<'a> Adapter<'a> for RustdocAdapter<'a> { "Attribute" => Box::new(data_contexts.map(move |ctx| { property_mapper(ctx, field_name.as_ref(), get_attribute_property) })), + "AttributeMetaItem" => Box::new(data_contexts.map(move |ctx| { + property_mapper(ctx, field_name.as_ref(), get_attribute_meta_item_property) + })), "ImplementedTrait" => Box::new(data_contexts.map(move |ctx| { property_mapper(ctx, field_name.as_ref(), get_implemented_trait_property) })), @@ -800,11 +836,9 @@ impl<'a> Adapter<'a> for RustdocAdapter<'a> { Some(token) => { let origin = token.origin; let item = token.as_item().expect("token was not an Item"); - Box::new( - item.attrs - .iter() - .map(move |attr| origin.make_attribute_token(attr)), - ) + Box::new(item.attrs.iter().map(move |attr| { + origin.make_attribute_token(Attribute::new(attr.as_str())) + })) } }; @@ -1173,6 +1207,56 @@ impl<'a> Adapter<'a> for RustdocAdapter<'a> { unreachable!("project_neighbors {current_type_name} {edge_name} {parameters:?}") } }, + "Attribute" => match edge_name.as_ref() { + "content" => Box::new(data_contexts.map(move |ctx| { + let neighbors: Box + 'a> = match &ctx + .current_token + { + None => Box::new(std::iter::empty()), + Some(token) => { + let origin = token.origin; + + let attribute = + token.as_attribute().expect("token was not an Attribute"); + Box::new(std::iter::once( + origin.make_attribute_meta_item_token(attribute.content.clone()), + )) + } + }; + + (ctx, neighbors) + })), + _ => { + unreachable!("project_neighbors {current_type_name} {edge_name} {parameters:?}") + } + }, + "AttributeMetaItem" => match edge_name.as_ref() { + "argument" => Box::new(data_contexts.map(move |ctx| { + let neighbors: Box + 'a> = + match &ctx.current_token { + None => Box::new(std::iter::empty()), + Some(token) => { + let origin = token.origin; + + let meta_item = token + .as_attribute_meta_item() + .expect("token was not an AttributeMetaItem"); + if let Some(arguments) = meta_item.arguments.clone() { + Box::new(arguments.into_iter().map(move |argument| { + origin.make_attribute_meta_item_token(argument) + })) + } else { + Box::new(std::iter::empty()) + } + } + }; + + (ctx, neighbors) + })), + _ => { + unreachable!("project_neighbors {current_type_name} {edge_name} {parameters:?}") + } + }, _ => unreachable!("project_neighbors {current_type_name} {edge_name} {parameters:?}"), } } diff --git a/src/attributes.rs b/src/attributes.rs new file mode 100644 index 0000000..7728bda --- /dev/null +++ b/src/attributes.rs @@ -0,0 +1,404 @@ +use std::rc::Rc; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Attribute<'a> { + pub is_inner: bool, + pub content: Rc>, +} + +impl<'a> Attribute<'a> { + pub fn raw_attribute(&self) -> String { + format!( + "#{}[{}]", + if self.is_inner { "!" } else { "" }, + self.content.raw_item + ) + } + + pub fn new(raw: &'a str) -> Self { + let raw_trimmed = raw.trim(); + let raw_without_closing = raw_trimmed.strip_suffix(']').unwrap_or_else(|| { + panic!( + "\ +String `{raw_trimmed}` cannot be parsed as an attribute \ +because it is not closed with a square bracket." + ) + }); + + if let Some(raw_content) = raw_without_closing.strip_prefix("#[") { + Attribute { + is_inner: false, + content: Rc::new(AttributeMetaItem::new(raw_content)), + } + } else if let Some(raw_content) = raw_without_closing.strip_prefix("#![") { + Attribute { + is_inner: true, + content: Rc::new(AttributeMetaItem::new(raw_content)), + } + } else { + panic!( + "\ +String `{raw_trimmed}` cannot be parsed as an attribute \ +because it starts with neither `#[` nor `#![`." + ) + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AttributeMetaItem<'a> { + pub raw_item: &'a str, + pub base: &'a str, + pub assigned_item: Option<&'a str>, + pub arguments: Option>>>, +} + +impl<'a> AttributeMetaItem<'a> { + fn is_left_bracket(c: char) -> bool { + c == '(' || c == '[' || c == '{' + } + + fn is_right_bracket(c: char) -> bool { + c == ')' || c == ']' || c == '}' + } + + fn matching_right_bracket(c: char) -> char { + match c { + '(' => ')', + '[' => ']', + '{' => '}', + _ => unreachable!("Tried to find matching right bracket for {c}."), + } + } + + /// Tries to parse `raw` as a comma-separated sequence of `AttributeMetaItem`'s + /// wrapped in parentheses, square brackets or curly brackets. + fn slice_arguments(raw: &'a str) -> Option>>> { + let raw_trimmed = raw.trim(); + let first_char = raw_trimmed.chars().next()?; + let raw_meta_seq = raw_trimmed + .strip_prefix(Self::is_left_bracket)? + .strip_suffix(|c| c == Self::matching_right_bracket(first_char))? + .trim(); + + let mut index_after_last_comma = 0; + let mut previous_is_escape = false; + let mut inside_string_literal = false; + let mut brackets = Vec::new(); // currently opened brackets + let mut arguments: Vec> = Vec::new(); // meta items constructed so far + + for (j, c) in raw_meta_seq.char_indices() { + if c == '"' && !previous_is_escape { + inside_string_literal = !inside_string_literal; + } + + if !inside_string_literal { + if Self::is_left_bracket(c) { + brackets.push(c); + } else if Self::is_right_bracket(c) { + // If the brackets don't match in any way, give up on parsing + // individual arguments since we don't understand the format. + if let Some(top_left) = brackets.pop() { + if Self::matching_right_bracket(top_left) != c { + return None; + } + } else { + return None; + } + } else if c == ',' { + // We only do a recursive call when the comma is on the outermost level. + if brackets.is_empty() { + arguments.push(Rc::new(AttributeMetaItem::new( + &raw_meta_seq[index_after_last_comma..j], + ))); + index_after_last_comma = j + 1; + } + } + } + + previous_is_escape = c == '\\'; + } + + // If the last comma was not a trailing one, there is still one meta item left. + if index_after_last_comma < raw_meta_seq.len() { + arguments.push(Rc::new(AttributeMetaItem::new( + &raw_meta_seq[index_after_last_comma..], + ))); + } + + Some(arguments) + } + + pub fn new(raw: &'a str) -> Self { + let raw_trimmed = raw.trim(); + + if let Some(path_end) = + raw_trimmed.find(|c: char| c.is_whitespace() || c == '=' || Self::is_left_bracket(c)) + { + let simple_path = &raw_trimmed[0..path_end]; + let attr_input = &raw_trimmed[path_end..]; + if !simple_path.is_empty() { + if let Some(assigned) = attr_input.trim().strip_prefix('=') { + return AttributeMetaItem { + raw_item: raw_trimmed, + base: simple_path, + assigned_item: Some(assigned.trim_start()), + arguments: None, + }; + } else if let Some(arguments) = Self::slice_arguments(attr_input) { + return AttributeMetaItem { + raw_item: raw_trimmed, + base: simple_path, + assigned_item: None, + arguments: Some(arguments), + }; + } + } + } + + AttributeMetaItem { + raw_item: raw_trimmed, + base: raw_trimmed, + assigned_item: None, + arguments: None, + } + } +} + +#[cfg(test)] +mod tests { + use std::rc::Rc; + + use super::{Attribute, AttributeMetaItem}; + + #[test] + fn attribute_simple_inner() { + let attribute = Attribute::new("#![no_std]"); + assert_eq!( + attribute, + Attribute { + is_inner: true, + content: Rc::new(AttributeMetaItem { + raw_item: "no_std", + base: "no_std", + assigned_item: None, + arguments: None + }) + } + ); + assert_eq!(attribute.raw_attribute(), "#![no_std]"); + } + + #[test] + fn attribute_complex_outer() { + let attribute = + Attribute::new("#[cfg_attr(feature = \"serde\", derive(Serialize, Deserialize))]"); + assert_eq!( + attribute, + Attribute { + is_inner: false, + content: Rc::new(AttributeMetaItem { + raw_item: "cfg_attr(feature = \"serde\", derive(Serialize, Deserialize))", + base: "cfg_attr", + assigned_item: None, + arguments: Some(vec![ + Rc::new(AttributeMetaItem { + raw_item: "feature = \"serde\"", + base: "feature", + assigned_item: Some("\"serde\""), + arguments: None + }), + Rc::new(AttributeMetaItem { + raw_item: "derive(Serialize, Deserialize)", + base: "derive", + assigned_item: None, + arguments: Some(vec![ + Rc::new(AttributeMetaItem { + raw_item: "Serialize", + base: "Serialize", + assigned_item: None, + arguments: None + }), + Rc::new(AttributeMetaItem { + raw_item: "Deserialize", + base: "Deserialize", + assigned_item: None, + arguments: None + }) + ]) + }) + ]) + }) + } + ); + } + + #[test] + fn attribute_unformatted() { + let attribute = Attribute::new("\t#[ derive ( Eq\t, PartialEq, ) ] "); + assert_eq!( + attribute, + Attribute { + is_inner: false, + content: Rc::new(AttributeMetaItem { + raw_item: "derive ( Eq\t, PartialEq, )", + base: "derive", + assigned_item: None, + arguments: Some(vec![ + Rc::new(AttributeMetaItem { + raw_item: "Eq", + base: "Eq", + assigned_item: None, + arguments: None + }), + Rc::new(AttributeMetaItem { + raw_item: "PartialEq", + base: "PartialEq", + assigned_item: None, + arguments: None + }) + ]) + }) + } + ); + assert_eq!( + attribute.raw_attribute(), + "#[derive ( Eq\t, PartialEq, )]" + ); + } + + #[test] + fn attribute_utf8() { + let attribute = Attribute::new("#[crate::gę42(bęc = \"🦀\", cśś = \"⭐\")]"); + assert_eq!( + attribute, + Attribute { + is_inner: false, + content: Rc::new(AttributeMetaItem { + raw_item: "crate::gę42(bęc = \"🦀\", cśś = \"⭐\")", + base: "crate::gę42", + assigned_item: None, + arguments: Some(vec![ + Rc::new(AttributeMetaItem { + raw_item: "bęc = \"🦀\"", + base: "bęc", + assigned_item: Some("\"🦀\""), + arguments: None + }), + Rc::new(AttributeMetaItem { + raw_item: "cśś = \"⭐\"", + base: "cśś", + assigned_item: Some("\"⭐\""), + arguments: None + }) + ]) + }) + } + ) + } + + #[test] + fn attribute_raw_identifier() { + let attribute = Attribute::new("#[r#derive(Debug)]"); + assert_eq!( + attribute, + Attribute { + is_inner: false, + content: Rc::new(AttributeMetaItem { + raw_item: "r#derive(Debug)", + base: "r#derive", + assigned_item: None, + arguments: Some(vec![Rc::new(AttributeMetaItem { + raw_item: "Debug", + base: "Debug", + assigned_item: None, + arguments: None + })]) + }) + } + ) + } + + #[test] + fn attribute_meta_item_custom_brackets() { + for raw_attribute in ["macro{arg1,arg2}", "macro[arg1,arg2]"] { + let meta_item = AttributeMetaItem::new(raw_attribute); + assert_eq!( + meta_item, + AttributeMetaItem { + raw_item: raw_attribute, + base: "macro", + assigned_item: None, + arguments: Some(vec![ + Rc::new(AttributeMetaItem { + raw_item: "arg1", + base: "arg1", + assigned_item: None, + arguments: None + }), + Rc::new(AttributeMetaItem { + raw_item: "arg2", + base: "arg2", + assigned_item: None, + arguments: None + }) + ]) + } + ); + } + } + + #[test] + fn attribute_meta_item_unrecognized_form() { + let meta_item = AttributeMetaItem::new("foo|bar|"); + assert_eq!( + meta_item, + AttributeMetaItem { + raw_item: "foo|bar|", + base: "foo|bar|", + assigned_item: None, + arguments: None + } + ); + } + + #[test] + fn attribute_meta_item_string_literals() { + let literals = [ + " ", + "comma ,", + "comma , escaped quote \\\" right parenthesis ) ", + "right parenthesis ) comma , left parenthesis (", + "right square ) comma , left square (", + "right curly } comma , left curly {", + "Mężny bądź, chroń pułk twój i sześć flag.", + ]; + + for literal in literals { + let raw_attribute = format!("foo(bar = \"{literal}\", baz = \"{literal}\")"); + let meta_item = AttributeMetaItem::new(&raw_attribute); + assert_eq!( + meta_item, + AttributeMetaItem { + raw_item: &raw_attribute, + base: "foo", + assigned_item: None, + arguments: Some(vec![ + Rc::new(AttributeMetaItem { + raw_item: format!("bar = \"{literal}\"").as_str(), + base: "bar", + assigned_item: Some(format!("\"{literal}\"").as_str()), + arguments: None + }), + Rc::new(AttributeMetaItem { + raw_item: format!("baz = \"{literal}\"").as_str(), + base: "baz", + assigned_item: Some(format!("\"{literal}\"").as_str()), + arguments: None + }) + ]) + } + ) + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 06ae5ca..12c856a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ mod adapter; +mod attributes; mod indexed_crate; // Re-export the Crate type so we can deserialize it. diff --git a/src/rustdoc_schema.graphql b/src/rustdoc_schema.graphql index c2329b9..84a4a63 100644 --- a/src/rustdoc_schema.graphql +++ b/src/rustdoc_schema.graphql @@ -517,11 +517,62 @@ A specific attribute applied to an Item. """ type Attribute { """ - The textual representation of the attribute. + String representation of the attribute as it is found in the code. - For example: "#[repr(C)]" + For example: `#[non_exhaustive]` """ - value: String! + raw_attribute: String! + + """ + True for an inner attribute (starting with `#![`), and false for an + outer one (starting with `#[`). + + For example: false for `#[non_exhaustive]` + """ + is_inner: Boolean! + + # edges + + # Edge to parsed content of the attribute + content: AttributeMetaItem! +} + +""" +A single meta item used by a specific attribute +(see https://doc.rust-lang.org/reference/attributes.html#meta-item-attribute-syntax). +""" +type AttributeMetaItem { + """ + The entire meta item represented as a string as it is found in the code. + + For example: `"derive(Debug, Clone)"` + """ + raw_item: String! + + """ + SimplePath of the meta item. + + For example: `"derive"` for `derive(Debug, Clone)`, + `"must_use"` for `must_use = "example_message"` + """ + base: String! + + """ + Assigned item if the meta item is in the form `SimplePath = AssignedItem`. + + For example: `"\"example_message\""` for `must_use = "example_message"` + """ + assigned_item: String + + # edges + + """ + Inner meta items if the meta item is in the form `SimplePath(MetaSeq)`. + + For example: `[AttributeMetaItem::new("Debug"), AttributeMetaItem::new("Clone")]` + for `derive(Debug, Clone)` + """ + argument: [AttributeMetaItem!] } """