pub mod bem;
pub mod children_slot;
pub mod clone_element;
pub mod diff;
pub mod managed_attrs;
pub mod prop_defaults;
pub mod prop_style;
pub mod react_api;
use bem::{extract_style_tokens, parse_bem_structure, StyleToken};
use children_slot::{has_children_prop, trace_children_slot_both};
use crate::sd_types::ComponentSourceProfile;
use managed_attrs::extract_managed_attributes;
use prop_defaults::extract_prop_defaults;
use prop_style::extract_prop_style_bindings;
use react_api::detect_react_api_usage;
use std::collections::{BTreeMap, BTreeSet};
use oxc_allocator::Allocator;
use oxc_ast::ast::*;
use oxc_parser::Parser;
use oxc_span::SourceType;
pub fn extract_profile(name: &str, file: &str, source: &str) -> ComponentSourceProfile {
let mut profile = ComponentSourceProfile {
name: name.to_string(),
file: file.to_string(),
..Default::default()
};
let ast_info = extract_source_info(source, name);
profile.rendered_elements = ast_info
.element_tags
.iter()
.filter(|(tag, _)| tag.starts_with(|c: char| c.is_lowercase()))
.map(|(k, v)| (k.clone(), *v as u32))
.collect();
profile.rendered_components = ast_info
.element_tags
.keys()
.filter(|tag| tag.starts_with(|c: char| c.is_uppercase()))
.map(|tag| crate::sd_types::RenderedComponent {
name: tag.clone(),
conditional: !ast_info.unconditional_tags.contains(tag),
})
.collect();
profile.aria_attributes = ast_info
.aria_attrs
.iter()
.map(|((elem, attr), val)| ((elem.clone(), attr.clone()), val.clone()))
.collect();
profile.role_attributes = ast_info.role_attrs.clone();
profile.data_attributes = ast_info
.data_attrs
.iter()
.map(|((elem, attr), val)| ((elem.clone(), attr.clone()), val.clone()))
.collect();
let style_tokens = extract_style_tokens(source);
for token in &style_tokens {
match token {
StyleToken::ClassToken(name) => {
profile.css_tokens_used.insert(format!("styles.{name}"));
}
StyleToken::Modifier(name) => {
profile
.css_tokens_used
.insert(format!("styles.modifiers.{name}"));
}
}
}
let bem = parse_bem_structure(&style_tokens, ast_info.styles_bem_block.as_deref());
profile.bem_block = bem.block;
profile.bem_elements = bem.elements;
profile.bem_modifiers = bem.modifiers;
let react_usage = detect_react_api_usage(source);
profile.uses_portal = react_usage.uses_portal;
profile.portal_target = react_usage.portal_target;
profile.consumed_contexts = react_usage.consumed_contexts;
profile.is_forward_ref = react_usage.is_forward_ref;
profile.is_memo = react_usage.is_memo;
profile.prop_defaults = extract_prop_defaults(source);
profile.has_children_prop = has_children_prop(source);
let (slot_path, slot_detail) = trace_children_slot_both(source);
profile.children_slot_path = slot_path;
profile.children_slot_detail = slot_detail;
profile.extends_props = ast_info.extends_props;
profile.all_props = ast_info.all_props;
profile.required_props = ast_info.required_props;
profile.prop_types = ast_info.prop_types;
profile.prop_style_bindings = extract_prop_style_bindings(source, &profile.all_props);
profile.provided_contexts = ast_info.context_providers;
for ctx_name in ast_info.context_consumers {
if !profile.consumed_contexts.contains(&ctx_name) {
profile.consumed_contexts.push(ctx_name);
}
}
profile.managed_attributes =
extract_managed_attributes(source, name, &profile.all_props, &profile.data_attributes);
profile.clone_element_injections = ast_info.clone_element_injections;
profile
}
pub(crate) fn kebab_to_camel_case(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut capitalize_next = false;
for ch in s.chars() {
if ch == '-' {
capitalize_next = true;
} else if capitalize_next {
result.push(ch.to_ascii_uppercase());
capitalize_next = false;
} else {
result.push(ch);
}
}
result
}
#[derive(Debug, Default)]
struct FullSourceInfo {
element_tags: BTreeMap<String, usize>,
unconditional_tags: BTreeSet<String>,
aria_attrs: BTreeMap<(String, String), String>,
role_attrs: BTreeMap<String, String>,
data_attrs: BTreeMap<(String, String), String>,
context_providers: Vec<String>,
context_consumers: Vec<String>,
styles_bem_block: Option<String>,
extends_props: Vec<String>,
all_props: BTreeSet<String>,
required_props: BTreeSet<String>,
prop_types: BTreeMap<String, String>,
clone_element_injections: Vec<crate::sd_types::CloneElementInjection>,
}
fn extract_source_info(source: &str, component_name: &str) -> FullSourceInfo {
let allocator = Allocator::default();
let source_type = SourceType::tsx();
let parsed = Parser::new(&allocator, source, source_type).parse();
let mut info = FullSourceInfo::default();
for stmt in &parsed.program.body {
extract_from_module_stmt(stmt, source, component_name, &mut info);
}
info
}
fn extract_from_module_stmt<'a>(
stmt: &'a Statement<'a>,
source: &str,
component_name: &str,
info: &mut FullSourceInfo,
) {
match stmt {
Statement::ImportDeclaration(import) => {
let src = import.source.value.as_str();
if src.contains("@patternfly/react-styles/css/") {
if let Some(specifiers) = &import.specifiers {
for spec in specifiers {
if let oxc_ast::ast::ImportDeclarationSpecifier::ImportDefaultSpecifier(
default_spec,
) = spec
{
if default_spec.local.name == "styles" {
if let Some(block) = src.rsplit('/').next() {
info.styles_bem_block = Some(kebab_to_camel_case(block));
}
}
}
}
}
}
}
Statement::ExportNamedDeclaration(export) => {
if let Some(decl) = &export.declaration {
extract_from_decl(decl, source, component_name, info);
}
}
Statement::ExportDefaultDeclaration(export) => {
if let Some(expr) = export.declaration.as_expression() {
walk_expr_for_jsx(expr, source, info, false);
}
}
Statement::TSInterfaceDeclaration(iface) => {
extract_extends_from_interface(iface, component_name, source, info);
}
_ => walk_stmt_for_jsx(stmt, source, info, false),
}
}
fn extract_from_decl<'a>(
decl: &'a Declaration<'a>,
source: &str,
component_name: &str,
info: &mut FullSourceInfo,
) {
match decl {
Declaration::TSInterfaceDeclaration(iface) => {
extract_extends_from_interface(iface, component_name, source, info);
}
Declaration::FunctionDeclaration(f) => {
if let Some(body) = &f.body {
walk_stmts_for_jsx(&body.statements, source, info, false);
}
}
Declaration::VariableDeclaration(var_decl) => {
for declarator in &var_decl.declarations {
if let Some(init) = &declarator.init {
walk_expr_for_jsx(init, source, info, false);
}
}
}
Declaration::ClassDeclaration(cls) => {
for item in &cls.body.body {
match item {
ClassElement::MethodDefinition(method) => {
if let Some(body) = &method.value.body {
walk_stmts_for_jsx(&body.statements, source, info, false);
}
}
ClassElement::PropertyDefinition(prop) => {
if let Some(init) = &prop.value {
walk_expr_for_jsx(init, source, info, false);
}
}
_ => {}
}
}
}
_ => {}
}
}
fn extract_extends_from_interface(
iface: &oxc_ast::ast::TSInterfaceDeclaration,
component_name: &str,
source: &str,
info: &mut FullSourceInfo,
) {
let iface_name = iface.id.name.as_str();
let props_name = format!("{}Props", component_name);
let base_props_name = format!("{}BaseProps", component_name);
if iface_name != props_name && iface_name != base_props_name {
return;
}
for heritage in &iface.extends {
let type_name = resolve_heritage_props_type(heritage);
if let Some(name) = type_name {
if name.ends_with("Props") && name != iface_name {
info.extends_props.push(name);
}
}
}
for sig in &iface.body.body {
if let oxc_ast::ast::TSSignature::TSPropertySignature(prop) = sig {
if let oxc_ast::ast::PropertyKey::StaticIdentifier(id) = &prop.key {
let prop_name = id.name.to_string();
info.all_props.insert(prop_name.clone());
if !prop.optional {
info.required_props.insert(prop_name.clone());
}
if let Some(type_ann) = &prop.type_annotation {
let type_str =
&source[type_ann.span.start as usize..type_ann.span.end as usize];
let type_str = type_str.trim_start_matches(':').trim();
if !type_str.is_empty() {
info.prop_types.insert(prop_name, type_str.to_string());
}
}
}
}
}
}
fn resolve_heritage_props_type(heritage: &oxc_ast::ast::TSInterfaceHeritage) -> Option<String> {
let expr_name = match &heritage.expression {
Expression::Identifier(id) => id.name.as_str(),
_ => return None,
};
if expr_name.ends_with("Props") {
return Some(expr_name.to_string());
}
if matches!(
expr_name,
"Omit" | "Partial" | "Pick" | "Required" | "Readonly"
) {
if let Some(type_args) = &heritage.type_arguments {
if let Some(oxc_ast::ast::TSType::TSTypeReference(type_ref)) = type_args.params.first()
{
if let oxc_ast::ast::TSTypeName::IdentifierReference(id) = &type_ref.type_name {
return Some(id.name.to_string());
}
}
}
}
None
}
fn walk_stmts_for_jsx<'a>(
stmts: &'a [Statement<'a>],
source: &str,
info: &mut FullSourceInfo,
conditional: bool,
) {
for stmt in stmts {
walk_stmt_for_jsx(stmt, source, info, conditional);
}
}
fn walk_stmt_for_jsx<'a>(
stmt: &'a Statement<'a>,
source: &str,
info: &mut FullSourceInfo,
conditional: bool,
) {
match stmt {
Statement::ClassDeclaration(cls) => {
for item in &cls.body.body {
match item {
ClassElement::MethodDefinition(method) => {
walk_params_for_jsx(&method.value.params, source, info, conditional);
if let Some(body) = &method.value.body {
walk_stmts_for_jsx(&body.statements, source, info, conditional);
}
}
ClassElement::PropertyDefinition(prop) => {
if let Some(init) = &prop.value {
walk_expr_for_jsx(init, source, info, conditional);
}
}
_ => {}
}
}
}
Statement::FunctionDeclaration(f) => {
walk_params_for_jsx(&f.params, source, info, conditional);
if let Some(body) = &f.body {
walk_stmts_for_jsx(&body.statements, source, info, conditional);
}
}
Statement::ReturnStatement(ret) => {
if let Some(expr) = &ret.argument {
walk_expr_for_jsx(expr, source, info, conditional);
}
}
Statement::ExpressionStatement(expr_stmt) => {
walk_expr_for_jsx(&expr_stmt.expression, source, info, conditional);
}
Statement::VariableDeclaration(decl) => {
for declarator in &decl.declarations {
walk_binding_defaults_for_jsx(&declarator.id, source, info, conditional);
if let Some(init) = &declarator.init {
walk_expr_for_jsx(init, source, info, conditional);
}
}
}
Statement::ExportNamedDeclaration(export) => {
if let Some(decl) = &export.declaration {
walk_decl_for_jsx(decl, source, info, conditional);
}
}
Statement::ExportDefaultDeclaration(export) => {
if let Some(expr) = export.declaration.as_expression() {
walk_expr_for_jsx(expr, source, info, conditional);
}
}
Statement::IfStatement(if_stmt) => {
walk_stmt_for_jsx(&if_stmt.consequent, source, info, true);
if let Some(alt) = &if_stmt.alternate {
walk_stmt_for_jsx(alt, source, info, true);
}
}
Statement::BlockStatement(block) => {
walk_stmts_for_jsx(&block.body, source, info, conditional);
}
_ => {}
}
}
fn walk_decl_for_jsx<'a>(
decl: &'a Declaration<'a>,
source: &str,
info: &mut FullSourceInfo,
conditional: bool,
) {
match decl {
Declaration::FunctionDeclaration(f) => {
walk_params_for_jsx(&f.params, source, info, conditional);
if let Some(body) = &f.body {
walk_stmts_for_jsx(&body.statements, source, info, conditional);
}
}
Declaration::VariableDeclaration(var_decl) => {
for declarator in &var_decl.declarations {
walk_binding_defaults_for_jsx(&declarator.id, source, info, conditional);
if let Some(init) = &declarator.init {
walk_expr_for_jsx(init, source, info, conditional);
}
}
}
Declaration::ClassDeclaration(cls) => {
for item in &cls.body.body {
match item {
ClassElement::MethodDefinition(method) => {
walk_params_for_jsx(&method.value.params, source, info, conditional);
if let Some(body) = &method.value.body {
walk_stmts_for_jsx(&body.statements, source, info, conditional);
}
}
ClassElement::PropertyDefinition(prop) => {
if let Some(init) = &prop.value {
walk_expr_for_jsx(init, source, info, conditional);
}
}
_ => {}
}
}
}
_ => {}
}
}
fn walk_binding_defaults_for_jsx<'a>(
pattern: &'a BindingPattern<'a>,
source: &str,
info: &mut FullSourceInfo,
conditional: bool,
) {
if let BindingPattern::ObjectPattern(obj) = pattern {
for prop in &obj.properties {
if let BindingPattern::AssignmentPattern(assign) = &prop.value {
walk_expr_for_jsx(&assign.right, source, info, conditional);
}
}
}
if let BindingPattern::AssignmentPattern(assign) = pattern {
if let BindingPattern::ObjectPattern(obj) = &assign.left {
for prop in &obj.properties {
if let BindingPattern::AssignmentPattern(inner) = &prop.value {
walk_expr_for_jsx(&inner.right, source, info, conditional);
}
}
}
}
}
fn walk_params_for_jsx<'a>(
params: &'a FormalParameters<'a>,
source: &str,
info: &mut FullSourceInfo,
conditional: bool,
) {
for param in ¶ms.items {
walk_binding_defaults_for_jsx(¶m.pattern, source, info, conditional);
}
}
fn walk_expr_for_jsx<'a>(
expr: &'a Expression<'a>,
source: &str,
info: &mut FullSourceInfo,
conditional: bool,
) {
match expr {
Expression::JSXElement(el) => visit_jsx_element_info(el, source, info, conditional),
Expression::JSXFragment(frag) => {
for child in &frag.children {
walk_jsx_child_info(child, source, info, conditional);
}
}
Expression::ParenthesizedExpression(paren) => {
walk_expr_for_jsx(&paren.expression, source, info, conditional);
}
Expression::ConditionalExpression(cond) => {
walk_expr_for_jsx(&cond.consequent, source, info, true);
walk_expr_for_jsx(&cond.alternate, source, info, true);
}
Expression::LogicalExpression(logical) => {
walk_expr_for_jsx(&logical.right, source, info, true);
}
Expression::CallExpression(call) => {
if let Some(injection) = clone_element::try_extract_clone_element_from_call(call) {
info.clone_element_injections.push(injection);
}
for arg in &call.arguments {
if let Some(expr) = arg.as_expression() {
walk_expr_for_jsx(expr, source, info, conditional);
}
}
}
Expression::TSAsExpression(ts_as) => {
walk_expr_for_jsx(&ts_as.expression, source, info, conditional);
}
Expression::TSSatisfiesExpression(ts_sat) => {
walk_expr_for_jsx(&ts_sat.expression, source, info, conditional);
}
Expression::TSNonNullExpression(ts_nn) => {
walk_expr_for_jsx(&ts_nn.expression, source, info, conditional);
}
Expression::TSTypeAssertion(ts_assert) => {
walk_expr_for_jsx(&ts_assert.expression, source, info, conditional);
}
Expression::TSInstantiationExpression(ts_inst) => {
walk_expr_for_jsx(&ts_inst.expression, source, info, conditional);
}
Expression::ArrowFunctionExpression(arrow) => {
walk_params_for_jsx(&arrow.params, source, info, conditional);
walk_stmts_for_jsx(&arrow.body.statements, source, info, conditional);
}
Expression::FunctionExpression(func) => {
walk_params_for_jsx(&func.params, source, info, conditional);
if let Some(body) = &func.body {
walk_stmts_for_jsx(&body.statements, source, info, conditional);
}
}
_ => {}
}
}
fn walk_jsx_child_info<'a>(
child: &'a JSXChild<'a>,
source: &str,
info: &mut FullSourceInfo,
conditional: bool,
) {
match child {
JSXChild::Element(el) => visit_jsx_element_info(el, source, info, conditional),
JSXChild::Fragment(frag) => {
for c in &frag.children {
walk_jsx_child_info(c, source, info, conditional);
}
}
JSXChild::ExpressionContainer(container) => {
if let Some(expr) = container.expression.as_expression() {
walk_expr_for_jsx(expr, source, info, conditional);
}
}
_ => {}
}
}
fn visit_jsx_element_info<'a>(
el: &'a JSXElement<'a>,
source: &str,
info: &mut FullSourceInfo,
conditional: bool,
) {
let tag_name = jsx_element_name_str(&el.opening_element.name);
*info.element_tags.entry(tag_name.clone()).or_insert(0) += 1;
if !conditional {
info.unconditional_tags.insert(tag_name.clone());
}
if let JSXElementName::MemberExpression(member) = &el.opening_element.name {
let prop = member.property.name.as_str();
if prop == "Provider" || prop == "Consumer" {
let ctx_name = jsx_member_obj_str(&member.object);
let list = if prop == "Provider" {
&mut info.context_providers
} else {
&mut info.context_consumers
};
if !list.contains(&ctx_name) {
list.push(ctx_name);
}
}
}
for attr_item in &el.opening_element.attributes {
if let JSXAttributeItem::Attribute(attr) = attr_item {
let attr_name = jsx_attr_name_str(&attr.name);
let attr_value = attr
.value
.as_ref()
.map(|v| jsx_attr_value_str(v, source))
.unwrap_or_default();
if attr_name.starts_with("aria-") {
info.aria_attrs
.insert((tag_name.clone(), attr_name), attr_value);
} else if attr_name == "role" {
info.role_attrs.insert(tag_name.clone(), attr_value);
} else if attr_name.starts_with("data-") {
info.data_attrs
.insert((tag_name.clone(), attr_name), attr_value);
}
}
}
for child in &el.children {
walk_jsx_child_info(child, source, info, conditional);
}
for attr_item in &el.opening_element.attributes {
if let JSXAttributeItem::Attribute(attr) = attr_item {
if let Some(JSXAttributeValue::ExpressionContainer(container)) = &attr.value {
if let Some(expr) = container.expression.as_expression() {
walk_expr_for_jsx(expr, source, info, conditional);
}
}
}
}
}
fn jsx_element_name_str(name: &JSXElementName) -> String {
match name {
JSXElementName::Identifier(id) => id.name.to_string(),
JSXElementName::IdentifierReference(id) => id.name.to_string(),
JSXElementName::NamespacedName(ns) => {
format!("{}:{}", ns.namespace.name, ns.name.name)
}
JSXElementName::MemberExpression(member) => {
format!(
"{}.{}",
jsx_member_obj_str(&member.object),
member.property.name
)
}
JSXElementName::ThisExpression(_) => "this".to_string(),
}
}
fn jsx_member_obj_str(obj: &JSXMemberExpressionObject) -> String {
match obj {
JSXMemberExpressionObject::IdentifierReference(id) => id.name.to_string(),
JSXMemberExpressionObject::MemberExpression(member) => {
format!(
"{}.{}",
jsx_member_obj_str(&member.object),
member.property.name
)
}
JSXMemberExpressionObject::ThisExpression(_) => "this".to_string(),
}
}
fn jsx_attr_name_str(name: &JSXAttributeName) -> String {
match name {
JSXAttributeName::Identifier(id) => id.name.to_string(),
JSXAttributeName::NamespacedName(ns) => {
format!("{}:{}", ns.namespace.name, ns.name.name)
}
}
}
fn jsx_attr_value_str(value: &JSXAttributeValue, source: &str) -> String {
match value {
JSXAttributeValue::StringLiteral(s) => s.value.to_string(),
JSXAttributeValue::ExpressionContainer(container) => {
let span = container.span;
source
.get(span.start as usize..span.end as usize)
.unwrap_or("")
.to_string()
}
_ => String::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_profile_simple() {
let source = r#"
import styles from '@patternfly/react-styles/css/components/Menu/menu';
import { css } from '@patternfly/react-styles';
export const MenuList = ({ children, className }: MenuListProps) => (
<ul className={css(styles.menuList, className)}>
{children}
</ul>
);
"#;
let profile = extract_profile(
"MenuList",
"packages/react-core/src/components/Menu/MenuList.tsx",
source,
);
assert_eq!(profile.name, "MenuList");
assert!(profile.rendered_elements.contains_key("ul"));
assert!(profile.has_children_prop);
assert_eq!(profile.children_slot_path, vec!["ul"]);
assert!(profile.css_tokens_used.contains("styles.menuList"));
}
#[test]
fn test_extract_profile_with_portal() {
let source = r#"
import * as ReactDOM from 'react-dom';
class Modal extends React.Component {
render() {
return ReactDOM.createPortal(
<ModalContent>{this.props.children}</ModalContent>,
this.getElement(this.props.appendTo)
);
}
}
export { Modal };
"#;
let profile = extract_profile("Modal", "Modal.tsx", source);
assert!(profile.uses_portal);
assert!(profile.portal_target.is_some());
}
#[test]
fn test_extract_profile_with_context() {
let source = r#"
import { useContext } from 'react';
import { AccordionItemContext } from './AccordionItemContext';
export const AccordionContent = ({ children }: Props) => {
const { isExpanded } = useContext(AccordionItemContext);
return isExpanded ? <div>{children}</div> : null;
};
"#;
let profile = extract_profile("AccordionContent", "AccordionContent.tsx", source);
assert!(profile
.consumed_contexts
.contains(&"AccordionItemContext".to_string()));
}
#[test]
fn test_extract_extends_props() {
let source = r#"
import { MenuListProps, MenuList } from '../Menu';
export interface DropdownListProps extends MenuListProps {
children: React.ReactNode;
className?: string;
}
"#;
let profile = extract_profile("DropdownList", "DropdownList.tsx", source);
assert_eq!(profile.extends_props, vec!["MenuListProps"]);
}
#[test]
fn test_extract_extends_props_multiple() {
let source = r#"
export interface DropdownProps extends MenuProps, OUIAProps {
children?: React.ReactNode;
}
"#;
let profile = extract_profile("Dropdown", "Dropdown.tsx", source);
assert_eq!(profile.extends_props, vec!["MenuProps", "OUIAProps"]);
}
#[test]
fn test_extract_extends_props_omit() {
let source = r#"
export interface DropdownItemProps extends Omit<MenuItemProps, 'ref'>, OUIAProps {
children?: React.ReactNode;
}
"#;
let profile = extract_profile("DropdownItem", "DropdownItem.tsx", source);
assert_eq!(profile.extends_props, vec!["MenuItemProps", "OUIAProps"]);
}
#[test]
fn test_extract_profile_class_component_context() {
let source = r#"
import { Component } from 'react';
import { MenuContext } from './MenuContext';
export interface MenuProps {
children?: React.ReactNode;
}
class MenuBase extends Component<MenuProps> {
render() {
return (
<MenuContext.Provider value={{ menuId: 'test' }}>
<div>{this.props.children}</div>
</MenuContext.Provider>
);
}
}
export const Menu = MenuBase;
"#;
let profile = extract_profile("Menu", "Menu.tsx", source);
assert!(
profile
.rendered_components
.iter()
.any(|r| r.name == "MenuContext.Provider"),
"Expected MenuContext.Provider in rendered_components, got: {:?}",
profile.rendered_components
);
assert!(
profile
.provided_contexts
.contains(&"MenuContext".to_string()),
"Expected MenuContext in provided_contexts, got: {:?}",
profile.provided_contexts
);
assert!(
profile.has_children_prop,
"Expected has_children_prop=true for class component with children?: React.ReactNode"
);
}
#[test]
fn test_extract_profile_with_defaults() {
let source = r#"
export const Button = ({
variant = 'primary',
isDisabled = false,
children,
}: ButtonProps) => (
<button disabled={isDisabled}>{children}</button>
);
"#;
let profile = extract_profile("Button", "Button.tsx", source);
assert_eq!(
profile.prop_defaults.get("variant"),
Some(&"'primary'".to_string())
);
assert_eq!(
profile.prop_defaults.get("isDisabled"),
Some(&"false".to_string())
);
}
#[test]
fn test_extract_profile_class_component_consumer() {
let source = r#"
import { Component } from 'react';
import { ToolbarContentContext, ToolbarContext } from './ToolbarUtils';
import { PageContext } from '../Page/PageContext';
class ToolbarToggleGroup extends Component<ToolbarToggleGroupProps> {
render() {
return (
<PageContext.Consumer>
{({ width }) => (
<ToolbarContext.Consumer>
{({ isExpanded }) => (
<ToolbarContentContext.Consumer>
{({ expandableContentRef }) => (
<div>{this.props.children}</div>
)}
</ToolbarContentContext.Consumer>
)}
</ToolbarContext.Consumer>
)}
</PageContext.Consumer>
);
}
}
export { ToolbarToggleGroup };
"#;
let profile = extract_profile("ToolbarToggleGroup", "ToolbarToggleGroup.tsx", source);
assert!(
profile
.consumed_contexts
.contains(&"ToolbarContentContext".to_string()),
"Expected ToolbarContentContext in consumed_contexts, got: {:?}",
profile.consumed_contexts
);
assert!(
profile
.consumed_contexts
.contains(&"ToolbarContext".to_string()),
"Expected ToolbarContext in consumed_contexts, got: {:?}",
profile.consumed_contexts
);
assert!(
profile
.consumed_contexts
.contains(&"PageContext".to_string()),
"Expected PageContext in consumed_contexts, got: {:?}",
profile.consumed_contexts
);
}
#[test]
fn test_extract_profile_class_component_provider_and_consumer() {
let source = r#"
import { Component } from 'react';
import { ToolbarContentContext, ToolbarContext } from './ToolbarUtils';
export interface ToolbarContentProps {
children?: React.ReactNode;
}
class ToolbarContent extends Component<ToolbarContentProps> {
render() {
return (
<ToolbarContext.Consumer>
{({ clearAllFilters }) => (
<ToolbarContentContext.Provider
value={{
expandableContentRef: this.expandableContentRef,
isExpanded: this.props.isExpanded,
}}
>
<div>{this.props.children}</div>
</ToolbarContentContext.Provider>
)}
</ToolbarContext.Consumer>
);
}
}
export { ToolbarContent };
"#;
let profile = extract_profile("ToolbarContent", "ToolbarContent.tsx", source);
assert!(
profile
.provided_contexts
.contains(&"ToolbarContentContext".to_string()),
"Expected ToolbarContentContext in provided_contexts, got: {:?}",
profile.provided_contexts
);
assert!(
profile
.consumed_contexts
.contains(&"ToolbarContext".to_string()),
"Expected ToolbarContext in consumed_contexts, got: {:?}",
profile.consumed_contexts
);
assert!(profile.has_children_prop);
}
#[test]
fn test_extract_profile_usecontext_and_consumer_merged() {
let source = r#"
import { useContext } from 'react';
import { AccordionItemContext } from './AccordionItemContext';
import { AccordionContext } from './AccordionContext';
export const AccordionToggle = ({ children }: Props) => {
const { isExpanded } = useContext(AccordionItemContext);
return (
<AccordionContext.Consumer>
{({ displaySize }) => (
<button>{children}</button>
)}
</AccordionContext.Consumer>
);
};
"#;
let profile = extract_profile("AccordionToggle", "AccordionToggle.tsx", source);
assert!(
profile
.consumed_contexts
.contains(&"AccordionItemContext".to_string()),
"Should detect useContext(AccordionItemContext)"
);
assert!(
profile
.consumed_contexts
.contains(&"AccordionContext".to_string()),
"Should detect <AccordionContext.Consumer>"
);
let unique: std::collections::HashSet<_> = profile.consumed_contexts.iter().collect();
assert_eq!(
unique.len(),
profile.consumed_contexts.len(),
"consumed_contexts should have no duplicates"
);
}
#[test]
fn test_extract_profile_arrow_param_default_jsx() {
let source = r#"
import { cloneElement } from 'react';
export const ChartBullet = ({
comparativeErrorMeasureComponent = <ChartBulletComparativeErrorMeasure />,
qualitativeRangeComponent = <ChartBulletQualitativeRange />,
titleComponent = <ChartBulletTitle />,
}: ChartBulletProps) => {
const measure = cloneElement(comparativeErrorMeasureComponent, { height: 100 });
return <div>{measure}</div>;
};
"#;
let profile = extract_profile("ChartBullet", "ChartBullet.tsx", source);
assert!(
profile
.rendered_components
.iter()
.any(|r| r.name == "ChartBulletComparativeErrorMeasure"),
"Expected ChartBulletComparativeErrorMeasure in rendered_components, got: {:?}",
profile.rendered_components
);
assert!(
profile
.rendered_components
.iter()
.any(|r| r.name == "ChartBulletQualitativeRange"),
"Expected ChartBulletQualitativeRange in rendered_components, got: {:?}",
profile.rendered_components
);
assert!(
profile
.rendered_components
.iter()
.any(|r| r.name == "ChartBulletTitle"),
"Expected ChartBulletTitle in rendered_components, got: {:?}",
profile.rendered_components
);
}
#[test]
fn test_extract_profile_class_render_destructuring_default_jsx() {
let source = r#"
import { Component } from 'react';
class ExpandableSection extends Component<ExpandableSectionProps> {
render() {
const {
toggleIcon = <RhMicronsCaretDownIcon />,
children,
} = this.props;
return (
<div>
{toggleIcon}
{children}
</div>
);
}
}
export { ExpandableSection };
"#;
let profile = extract_profile("ExpandableSection", "ExpandableSection.tsx", source);
assert!(
profile
.rendered_components
.iter()
.any(|r| r.name == "RhMicronsCaretDownIcon"),
"Expected RhMicronsCaretDownIcon in rendered_components, got: {:?}",
profile.rendered_components
);
}
#[test]
fn test_extract_profile_dynamic_component_td() {
let source = r#"
const TdBase = ({
children,
component = 'td',
className,
}: TdProps) => {
const merged = mergeProps({ component });
const {
component: MergedComponent = component,
children: mergedChildren = null,
} = merged;
const cell = (
<MergedComponent className={className}>
{mergedChildren || children}
</MergedComponent>
);
return cell;
};
export const Td = forwardRef((props: TdProps, ref) => (
<TdBase {...props} innerRef={ref} />
));
"#;
let profile = extract_profile("Td", "Td.tsx", source);
eprintln!("Td children_slot_path: {:?}", profile.children_slot_path);
eprintln!("Td has_children_prop: {}", profile.has_children_prop);
eprintln!("Td rendered_elements: {:?}", profile.rendered_elements);
assert_eq!(profile.children_slot_path, vec!["td"]);
}
#[test]
fn test_extract_profile_class_property_definition_render() {
let source = r#"
import { Component } from 'react';
import { ClipboardCopyButton } from './ClipboardCopyButton';
import { ClipboardCopyToggle } from './ClipboardCopyToggle';
class ClipboardCopy extends Component<ClipboardCopyProps, ClipboardCopyState> {
timer = null as any;
render = () => {
const { children } = this.props;
return (
<div>
<ClipboardCopyToggle />
<input value={this.state.text} />
<ClipboardCopyButton onClick={this.handleCopy}>
Copy
</ClipboardCopyButton>
{children}
</div>
);
};
}
export { ClipboardCopy };
"#;
let profile = extract_profile("ClipboardCopy", "ClipboardCopy.tsx", source);
assert!(
profile
.rendered_components
.iter()
.any(|r| r.name == "ClipboardCopyButton"),
"Expected ClipboardCopyButton in rendered_components, got: {:?}",
profile.rendered_components
);
assert!(
profile
.rendered_components
.iter()
.any(|r| r.name == "ClipboardCopyToggle"),
"Expected ClipboardCopyToggle in rendered_components, got: {:?}",
profile.rendered_components
);
}
#[test]
fn test_conditional_rendering_ternary() {
let source = r#"
import React from 'react';
const Comp = ({ show }) => {
return (
<div>
<AlwaysRendered />
{show ? <ConditionalA /> : <ConditionalB />}
</div>
);
};
export { Comp };
"#;
let profile = extract_profile("Comp", "Comp.tsx", source);
let always = profile
.rendered_components
.iter()
.find(|r| r.name == "AlwaysRendered");
assert!(always.is_some(), "AlwaysRendered should be present");
assert!(
!always.unwrap().conditional,
"AlwaysRendered should be unconditional"
);
let cond_a = profile
.rendered_components
.iter()
.find(|r| r.name == "ConditionalA");
assert!(cond_a.is_some(), "ConditionalA should be present");
assert!(
cond_a.unwrap().conditional,
"ConditionalA should be conditional (inside ternary)"
);
let cond_b = profile
.rendered_components
.iter()
.find(|r| r.name == "ConditionalB");
assert!(cond_b.is_some(), "ConditionalB should be present");
assert!(
cond_b.unwrap().conditional,
"ConditionalB should be conditional (inside ternary)"
);
}
#[test]
fn test_conditional_rendering_logical_and() {
let source = r#"
import React from 'react';
const Comp = ({ show }) => {
return (
<div>
<Header />
{show && <OptionalFooter />}
</div>
);
};
export { Comp };
"#;
let profile = extract_profile("Comp", "Comp.tsx", source);
let header = profile
.rendered_components
.iter()
.find(|r| r.name == "Header");
assert!(
!header.unwrap().conditional,
"Header should be unconditional"
);
let footer = profile
.rendered_components
.iter()
.find(|r| r.name == "OptionalFooter");
assert!(
footer.unwrap().conditional,
"OptionalFooter should be conditional (inside &&)"
);
}
#[test]
fn test_conditional_rendering_if_statement() {
let source = r#"
import React from 'react';
function Comp({ variant }) {
if (variant === 'a') {
return <VariantA />;
}
return <Default />;
}
export { Comp };
"#;
let profile = extract_profile("Comp", "Comp.tsx", source);
let variant_a = profile
.rendered_components
.iter()
.find(|r| r.name == "VariantA");
assert!(
variant_a.unwrap().conditional,
"VariantA should be conditional (inside if branch)"
);
let default = profile
.rendered_components
.iter()
.find(|r| r.name == "Default");
assert!(
!default.unwrap().conditional,
"Default should be unconditional (bare return)"
);
}
#[test]
fn test_conditional_rendering_map_is_unconditional() {
let source = r#"
import React from 'react';
const List = ({ items }) => {
return (
<ul>
{items.map(item => <ListItem key={item.id} />)}
</ul>
);
};
export { List };
"#;
let profile = extract_profile("List", "List.tsx", source);
let item = profile
.rendered_components
.iter()
.find(|r| r.name == "ListItem");
assert!(
!item.unwrap().conditional,
"ListItem inside .map() should be unconditional"
);
}
#[test]
fn test_unconditional_wins_over_conditional() {
let source = r#"
import React from 'react';
const Comp = ({ extra }) => {
return (
<div>
<Child />
{extra && <Child />}
</div>
);
};
export { Comp };
"#;
let profile = extract_profile("Comp", "Comp.tsx", source);
let child = profile
.rendered_components
.iter()
.find(|r| r.name == "Child");
assert!(
!child.unwrap().conditional,
"Child should be unconditional (appears in both conditional and unconditional contexts)"
);
}
}