use crate::sd_types::ManagedAttributeBinding;
use oxc_allocator::Allocator;
use oxc_ast::ast::*;
use oxc_parser::Parser;
use oxc_span::SourceType;
use std::collections::{BTreeMap, BTreeSet, HashSet};
#[derive(Debug, Default)]
struct PropsDestructuring {
named_props: HashSet<String>,
rest_name: Option<String>,
}
#[derive(Debug)]
struct PropFunctionFlow {
prop_args: Vec<String>,
function_name: String,
result_variable: String,
}
#[derive(Debug)]
struct JsxSpreadEntry {
position: usize,
variable_name: String,
}
#[derive(Debug)]
struct JsxElementSpreads {
tag_name: String,
spreads: Vec<JsxSpreadEntry>,
}
#[derive(Debug, Default)]
struct RestPropagation {
rest_carriers: HashSet<String>,
}
pub fn extract_managed_attributes(
source: &str,
_component_name: &str,
known_props: &BTreeSet<String>,
data_attributes: &BTreeMap<(String, String), String>,
) -> Vec<ManagedAttributeBinding> {
let allocator = Allocator::default();
let source_type = SourceType::tsx();
let parsed = Parser::new(&allocator, source, source_type).parse();
let mut destructurings = Vec::new();
let mut function_flows = Vec::new();
let mut jsx_spreads = Vec::new();
let mut rest_propagation = RestPropagation::default();
for stmt in &parsed.program.body {
collect_from_stmt(
stmt,
source,
known_props,
&mut destructurings,
&mut function_flows,
&mut jsx_spreads,
&mut rest_propagation,
);
}
let primary = match destructurings.into_iter().find(|d| d.rest_name.is_some()) {
Some(d) => d,
None => return Vec::new(),
};
let rest_name = primary.rest_name.as_deref().unwrap();
rest_propagation.rest_carriers.insert(rest_name.to_string());
build_bindings(
&primary,
&function_flows,
&jsx_spreads,
&rest_propagation,
data_attributes,
)
}
fn build_bindings(
destructuring: &PropsDestructuring,
flows: &[PropFunctionFlow],
jsx_spreads: &[JsxElementSpreads],
rest_prop: &RestPropagation,
data_attributes: &BTreeMap<(String, String), String>,
) -> Vec<ManagedAttributeBinding> {
let mut bindings = Vec::new();
for flow in flows {
if flow
.prop_args
.iter()
.all(|p| !destructuring.named_props.contains(p))
{
continue;
}
for element in jsx_spreads {
let result_spread_pos = element.spreads.iter().find_map(|s| {
if s.variable_name == flow.result_variable {
Some(s.position)
} else {
None
}
});
let result_pos = match result_spread_pos {
Some(pos) => pos,
None => continue,
};
let has_rest_before = element.spreads.iter().any(|s| {
s.position < result_pos && rest_prop.rest_carriers.contains(&s.variable_name)
});
let overridden: Vec<String> = data_attributes
.keys()
.filter(|(elem, _attr)| elem == &element.tag_name)
.map(|(_elem, attr)| attr.clone())
.collect();
for prop_name in &flow.prop_args {
if destructuring.named_props.contains(prop_name) {
bindings.push(ManagedAttributeBinding {
prop_name: prop_name.clone(),
generator_function: flow.function_name.clone(),
target_element: element.tag_name.clone(),
overridden_attributes: overridden.clone(),
component_overrides: has_rest_before,
});
}
}
}
}
let mut seen = HashSet::new();
bindings.retain(|b| {
seen.insert((
b.prop_name.clone(),
b.generator_function.clone(),
b.target_element.clone(),
))
});
bindings
}
fn collect_from_stmt<'a>(
stmt: &'a Statement<'a>,
source: &str,
known_props: &BTreeSet<String>,
destructurings: &mut Vec<PropsDestructuring>,
flows: &mut Vec<PropFunctionFlow>,
jsx_spreads: &mut Vec<JsxElementSpreads>,
rest_prop: &mut RestPropagation,
) {
match stmt {
Statement::ExportNamedDeclaration(export) => {
if let Some(decl) = &export.declaration {
collect_from_decl(
decl,
source,
known_props,
destructurings,
flows,
jsx_spreads,
rest_prop,
);
}
}
Statement::ExportDefaultDeclaration(export) => {
if let Some(expr) = export.declaration.as_expression() {
collect_from_expr(
expr,
source,
known_props,
destructurings,
flows,
jsx_spreads,
rest_prop,
);
}
}
Statement::VariableDeclaration(decl) => {
for declarator in &decl.declarations {
if let Some(init) = &declarator.init {
check_destructuring_declarator(
declarator,
init,
source,
known_props,
destructurings,
rest_prop,
);
check_function_flow(declarator, init, source, known_props, flows);
check_rest_propagation(declarator, init, rest_prop);
collect_from_expr(
init,
source,
known_props,
destructurings,
flows,
jsx_spreads,
rest_prop,
);
}
}
}
Statement::ReturnStatement(ret) => {
if let Some(expr) = &ret.argument {
collect_from_expr(
expr,
source,
known_props,
destructurings,
flows,
jsx_spreads,
rest_prop,
);
}
}
Statement::ExpressionStatement(expr_stmt) => {
collect_from_expr(
&expr_stmt.expression,
source,
known_props,
destructurings,
flows,
jsx_spreads,
rest_prop,
);
}
Statement::IfStatement(if_stmt) => {
collect_from_stmt(
&if_stmt.consequent,
source,
known_props,
destructurings,
flows,
jsx_spreads,
rest_prop,
);
if let Some(alt) = &if_stmt.alternate {
collect_from_stmt(
alt,
source,
known_props,
destructurings,
flows,
jsx_spreads,
rest_prop,
);
}
}
Statement::BlockStatement(block) => {
for s in &block.body {
collect_from_stmt(
s,
source,
known_props,
destructurings,
flows,
jsx_spreads,
rest_prop,
);
}
}
Statement::FunctionDeclaration(f) => {
check_function_params(&f.params, source, known_props, destructurings);
if let Some(body) = &f.body {
for s in &body.statements {
collect_from_stmt(
s,
source,
known_props,
destructurings,
flows,
jsx_spreads,
rest_prop,
);
}
}
}
Statement::ClassDeclaration(cls) => {
for item in &cls.body.body {
match item {
ClassElement::MethodDefinition(method) => {
if let Some(body) = &method.value.body {
for s in &body.statements {
collect_from_stmt(
s,
source,
known_props,
destructurings,
flows,
jsx_spreads,
rest_prop,
);
}
}
}
ClassElement::PropertyDefinition(prop) => {
if let Some(init) = &prop.value {
collect_from_expr(
init,
source,
known_props,
destructurings,
flows,
jsx_spreads,
rest_prop,
);
}
}
_ => {}
}
}
}
_ => {}
}
}
fn collect_from_decl<'a>(
decl: &'a Declaration<'a>,
source: &str,
known_props: &BTreeSet<String>,
destructurings: &mut Vec<PropsDestructuring>,
flows: &mut Vec<PropFunctionFlow>,
jsx_spreads: &mut Vec<JsxElementSpreads>,
rest_prop: &mut RestPropagation,
) {
match decl {
Declaration::FunctionDeclaration(f) => {
check_function_params(&f.params, source, known_props, destructurings);
if let Some(body) = &f.body {
for s in &body.statements {
collect_from_stmt(
s,
source,
known_props,
destructurings,
flows,
jsx_spreads,
rest_prop,
);
}
}
}
Declaration::VariableDeclaration(var_decl) => {
for declarator in &var_decl.declarations {
if let Some(init) = &declarator.init {
check_destructuring_declarator(
declarator,
init,
source,
known_props,
destructurings,
rest_prop,
);
check_function_flow(declarator, init, source, known_props, flows);
check_rest_propagation(declarator, init, rest_prop);
collect_from_expr(
init,
source,
known_props,
destructurings,
flows,
jsx_spreads,
rest_prop,
);
}
}
}
Declaration::ClassDeclaration(cls) => {
for item in &cls.body.body {
match item {
ClassElement::MethodDefinition(method) => {
check_function_params(
&method.value.params,
source,
known_props,
destructurings,
);
if let Some(body) = &method.value.body {
for s in &body.statements {
collect_from_stmt(
s,
source,
known_props,
destructurings,
flows,
jsx_spreads,
rest_prop,
);
}
}
}
ClassElement::PropertyDefinition(prop) => {
if let Some(init) = &prop.value {
collect_from_expr(
init,
source,
known_props,
destructurings,
flows,
jsx_spreads,
rest_prop,
);
}
}
_ => {}
}
}
}
_ => {}
}
}
fn collect_from_expr<'a>(
expr: &'a Expression<'a>,
source: &str,
known_props: &BTreeSet<String>,
destructurings: &mut Vec<PropsDestructuring>,
flows: &mut Vec<PropFunctionFlow>,
jsx_spreads: &mut Vec<JsxElementSpreads>,
rest_prop: &mut RestPropagation,
) {
match expr {
Expression::JSXElement(el) => {
collect_jsx_spreads(el, source, known_props, jsx_spreads, flows);
for child in &el.children {
collect_from_jsx_child(
child,
source,
known_props,
destructurings,
flows,
jsx_spreads,
rest_prop,
);
}
}
Expression::JSXFragment(frag) => {
for child in &frag.children {
collect_from_jsx_child(
child,
source,
known_props,
destructurings,
flows,
jsx_spreads,
rest_prop,
);
}
}
Expression::ParenthesizedExpression(paren) => {
collect_from_expr(
&paren.expression,
source,
known_props,
destructurings,
flows,
jsx_spreads,
rest_prop,
);
}
Expression::ConditionalExpression(cond) => {
collect_from_expr(
&cond.consequent,
source,
known_props,
destructurings,
flows,
jsx_spreads,
rest_prop,
);
collect_from_expr(
&cond.alternate,
source,
known_props,
destructurings,
flows,
jsx_spreads,
rest_prop,
);
}
Expression::LogicalExpression(logical) => {
collect_from_expr(
&logical.right,
source,
known_props,
destructurings,
flows,
jsx_spreads,
rest_prop,
);
}
Expression::CallExpression(call) => {
for arg in &call.arguments {
if let Some(e) = arg.as_expression() {
collect_from_expr(
e,
source,
known_props,
destructurings,
flows,
jsx_spreads,
rest_prop,
);
}
}
}
Expression::ArrowFunctionExpression(arrow) => {
check_function_params(&arrow.params, source, known_props, destructurings);
for s in &arrow.body.statements {
collect_from_stmt(
s,
source,
known_props,
destructurings,
flows,
jsx_spreads,
rest_prop,
);
}
}
Expression::FunctionExpression(func) => {
check_function_params(&func.params, source, known_props, destructurings);
if let Some(body) = &func.body {
for s in &body.statements {
collect_from_stmt(
s,
source,
known_props,
destructurings,
flows,
jsx_spreads,
rest_prop,
);
}
}
}
_ => {}
}
}
fn collect_from_jsx_child<'a>(
child: &'a JSXChild<'a>,
source: &str,
known_props: &BTreeSet<String>,
destructurings: &mut Vec<PropsDestructuring>,
flows: &mut Vec<PropFunctionFlow>,
jsx_spreads: &mut Vec<JsxElementSpreads>,
rest_prop: &mut RestPropagation,
) {
match child {
JSXChild::Element(el) => {
collect_jsx_spreads(el, source, known_props, jsx_spreads, flows);
for c in &el.children {
collect_from_jsx_child(
c,
source,
known_props,
destructurings,
flows,
jsx_spreads,
rest_prop,
);
}
}
JSXChild::Fragment(frag) => {
for c in &frag.children {
collect_from_jsx_child(
c,
source,
known_props,
destructurings,
flows,
jsx_spreads,
rest_prop,
);
}
}
JSXChild::ExpressionContainer(container) => {
if let Some(expr) = container.expression.as_expression() {
collect_from_expr(
expr,
source,
known_props,
destructurings,
flows,
jsx_spreads,
rest_prop,
);
}
}
_ => {}
}
}
fn check_function_params(
params: &FormalParameters,
_source: &str,
known_props: &BTreeSet<String>,
destructurings: &mut Vec<PropsDestructuring>,
) {
for param in ¶ms.items {
check_binding_pattern(¶m.pattern, known_props, destructurings);
}
}
fn check_binding_pattern(
pattern: &BindingPattern,
known_props: &BTreeSet<String>,
destructurings: &mut Vec<PropsDestructuring>,
) {
match pattern {
BindingPattern::ObjectPattern(obj) => {
extract_destructuring_from_object_pattern(obj, known_props, destructurings);
}
BindingPattern::AssignmentPattern(assign) => {
check_binding_pattern(&assign.left, known_props, destructurings);
}
_ => {}
}
}
fn extract_destructuring_from_object_pattern(
obj: &ObjectPattern,
known_props: &BTreeSet<String>,
destructurings: &mut Vec<PropsDestructuring>,
) {
let mut destr = PropsDestructuring::default();
for prop in &obj.properties {
if let PropertyKey::StaticIdentifier(id) = &prop.key {
let name = id.name.to_string();
if known_props.contains(&name) || known_props.is_empty() {
destr.named_props.insert(name);
}
}
}
if let Some(rest) = &obj.rest {
if let BindingPattern::BindingIdentifier(id) = &rest.argument {
destr.rest_name = Some(id.name.to_string());
}
}
if !destr.named_props.is_empty() || destr.rest_name.is_some() {
destructurings.push(destr);
}
}
fn check_destructuring_declarator<'a>(
declarator: &'a VariableDeclarator<'a>,
init: &'a Expression<'a>,
_source: &str,
known_props: &BTreeSet<String>,
destructurings: &mut Vec<PropsDestructuring>,
rest_prop: &mut RestPropagation,
) {
let is_this_props = matches!(init, Expression::StaticMemberExpression(member)
if matches!(&member.object, Expression::ThisExpression(_))
&& member.property.name == "props"
);
if !is_this_props {
return;
}
if let BindingPattern::ObjectPattern(obj) = &declarator.id {
extract_destructuring_from_object_pattern(obj, known_props, destructurings);
if let Some(rest) = &obj.rest {
if let BindingPattern::BindingIdentifier(id) = &rest.argument {
rest_prop.rest_carriers.insert(id.name.to_string());
}
}
}
}
fn check_function_flow<'a>(
declarator: &'a VariableDeclarator<'a>,
init: &'a Expression<'a>,
source: &str,
known_props: &BTreeSet<String>,
flows: &mut Vec<PropFunctionFlow>,
) {
let var_name = match &declarator.id {
BindingPattern::BindingIdentifier(id) => id.name.to_string(),
_ => return,
};
let call = match init {
Expression::CallExpression(call) => call,
_ => return,
};
let func_name = match &call.callee {
Expression::Identifier(id) => id.name.to_string(),
Expression::StaticMemberExpression(member) => {
format!(
"{}.{}",
expr_name(&member.object, source),
member.property.name
)
}
_ => return,
};
let mut prop_args = Vec::new();
for arg in &call.arguments {
if let Some(expr) = arg.as_expression() {
collect_prop_refs_from_expr(expr, source, known_props, &mut prop_args);
}
}
if !prop_args.is_empty() {
flows.push(PropFunctionFlow {
prop_args,
function_name: func_name,
result_variable: var_name,
});
}
}
fn collect_prop_refs_from_expr(
expr: &Expression,
_source: &str,
known_props: &BTreeSet<String>,
out: &mut Vec<String>,
) {
match expr {
Expression::Identifier(id) => {
let name = id.name.to_string();
if known_props.contains(&name) && !out.contains(&name) {
out.push(name);
}
}
Expression::LogicalExpression(logical) => {
collect_prop_refs_from_expr(&logical.left, _source, known_props, out);
collect_prop_refs_from_expr(&logical.right, _source, known_props, out);
}
Expression::ConditionalExpression(cond) => {
collect_prop_refs_from_expr(&cond.test, _source, known_props, out);
collect_prop_refs_from_expr(&cond.consequent, _source, known_props, out);
collect_prop_refs_from_expr(&cond.alternate, _source, known_props, out);
}
Expression::ParenthesizedExpression(paren) => {
collect_prop_refs_from_expr(&paren.expression, _source, known_props, out);
}
_ => {}
}
}
fn check_rest_propagation<'a>(
declarator: &'a VariableDeclarator<'a>,
init: &'a Expression<'a>,
rest_prop: &mut RestPropagation,
) {
let var_name = match &declarator.id {
BindingPattern::BindingIdentifier(id) => id.name.to_string(),
_ => return,
};
let obj = match init {
Expression::ObjectExpression(obj) => obj,
_ => return,
};
for prop in &obj.properties {
if let ObjectPropertyKind::SpreadProperty(spread) = prop {
if let Expression::Identifier(id) = &spread.argument {
if rest_prop.rest_carriers.contains(id.name.as_str()) {
rest_prop.rest_carriers.insert(var_name);
return;
}
}
}
}
}
fn collect_jsx_spreads<'a>(
el: &'a JSXElement<'a>,
source: &str,
known_props: &BTreeSet<String>,
jsx_spreads: &mut Vec<JsxElementSpreads>,
function_flows: &mut Vec<PropFunctionFlow>,
) {
let tag_name = jsx_element_tag_name(&el.opening_element.name);
let mut spreads = Vec::new();
for (position, attr_item) in el.opening_element.attributes.iter().enumerate() {
if let JSXAttributeItem::SpreadAttribute(spread) = attr_item {
let variable_name = expr_name(&spread.argument, source);
if !variable_name.is_empty() {
spreads.push(JsxSpreadEntry {
position,
variable_name,
});
} else {
if let Some((func_name, prop_args)) =
extract_inline_call(&spread.argument, source, known_props)
{
let synthetic_name = format!("__inline_{func_name}_{position}");
spreads.push(JsxSpreadEntry {
position,
variable_name: synthetic_name.clone(),
});
function_flows.push(PropFunctionFlow {
prop_args,
function_name: func_name,
result_variable: synthetic_name,
});
}
}
}
}
if !spreads.is_empty() {
jsx_spreads.push(JsxElementSpreads { tag_name, spreads });
}
for child in &el.children {
collect_jsx_spreads_from_child(child, source, known_props, jsx_spreads, function_flows);
}
}
fn collect_jsx_spreads_from_child<'a>(
child: &'a JSXChild<'a>,
source: &str,
known_props: &BTreeSet<String>,
jsx_spreads: &mut Vec<JsxElementSpreads>,
function_flows: &mut Vec<PropFunctionFlow>,
) {
match child {
JSXChild::Element(el) => {
collect_jsx_spreads(el, source, known_props, jsx_spreads, function_flows);
}
JSXChild::Fragment(frag) => {
for c in &frag.children {
collect_jsx_spreads_from_child(c, source, known_props, jsx_spreads, function_flows);
}
}
_ => {}
}
}
fn extract_inline_call(
expr: &Expression,
source: &str,
known_props: &BTreeSet<String>,
) -> Option<(String, Vec<String>)> {
match expr {
Expression::CallExpression(call) => {
let func_name = match &call.callee {
Expression::Identifier(id) => id.name.to_string(),
Expression::StaticMemberExpression(member) => {
format!(
"{}.{}",
expr_name(&member.object, source),
member.property.name
)
}
_ => return None,
};
let mut prop_args = Vec::new();
for arg in &call.arguments {
if let Some(arg_expr) = arg.as_expression() {
collect_prop_refs_from_expr(arg_expr, source, known_props, &mut prop_args);
}
}
if prop_args.is_empty() {
return None;
}
Some((func_name, prop_args))
}
Expression::TSAsExpression(ts) => extract_inline_call(&ts.expression, source, known_props),
Expression::TSSatisfiesExpression(ts) => {
extract_inline_call(&ts.expression, source, known_props)
}
Expression::TSNonNullExpression(ts) => {
extract_inline_call(&ts.expression, source, known_props)
}
Expression::TSTypeAssertion(ts) => extract_inline_call(&ts.expression, source, known_props),
Expression::ParenthesizedExpression(paren) => {
extract_inline_call(&paren.expression, source, known_props)
}
Expression::ConditionalExpression(cond) => {
extract_inline_call(&cond.consequent, source, known_props)
.or_else(|| extract_inline_call(&cond.alternate, source, known_props))
}
Expression::LogicalExpression(logical) => {
extract_inline_call(&logical.right, source, known_props)
.or_else(|| extract_inline_call(&logical.left, source, known_props))
}
_ => None,
}
}
fn expr_name(expr: &Expression, _source: &str) -> String {
match expr {
Expression::Identifier(id) => id.name.to_string(),
Expression::ThisExpression(_) => "this".to_string(),
Expression::StaticMemberExpression(member) => {
let obj = expr_name(&member.object, _source);
format!("{}.{}", obj, member.property.name)
}
_ => String::new(),
}
}
fn jsx_element_tag_name(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_name(&member.object),
member.property.name
)
}
JSXElementName::ThisExpression(_) => "this".to_string(),
}
}
fn jsx_member_obj_name(obj: &JSXMemberExpressionObject) -> String {
match obj {
JSXMemberExpressionObject::IdentifierReference(id) => id.name.to_string(),
JSXMemberExpressionObject::MemberExpression(member) => {
format!(
"{}.{}",
jsx_member_obj_name(&member.object),
member.property.name
)
}
JSXMemberExpressionObject::ThisExpression(_) => "this".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn props(names: &[&str]) -> BTreeSet<String> {
names.iter().map(|s| s.to_string()).collect()
}
fn data_attrs(entries: &[(&str, &str)]) -> BTreeMap<(String, String), String> {
entries
.iter()
.map(|(elem, attr)| ((elem.to_string(), attr.to_string()), String::new()))
.collect()
}
#[test]
fn test_class_component_ouia_pattern() {
let source = r#"
import { getOUIAProps } from '../../helpers';
export interface MenuToggleProps {
ouiaId?: number | string;
ouiaSafe?: boolean;
children?: React.ReactNode;
}
class MenuToggleBase extends React.Component {
render() {
const {
children, ouiaId, ouiaSafe, ...otherProps
} = this.props;
const ouiaProps = getOUIAProps('MenuToggle', ouiaId, ouiaSafe);
const componentProps = {
children: children,
...otherProps,
};
return (
<button {...componentProps} {...ouiaProps} />
);
}
}
"#;
let known = props(&["ouiaId", "ouiaSafe", "children"]);
let data = data_attrs(&[
("button", "data-ouia-component-id"),
("button", "data-ouia-component-type"),
("button", "data-ouia-safe"),
]);
let bindings = extract_managed_attributes(source, "MenuToggle", &known, &data);
assert!(
!bindings.is_empty(),
"Expected managed attribute bindings, got none"
);
let ouia_binding = bindings.iter().find(|b| b.prop_name == "ouiaId");
assert!(
ouia_binding.is_some(),
"Expected ouiaId binding, found: {:?}",
bindings
);
let binding = ouia_binding.unwrap();
assert_eq!(binding.generator_function, "getOUIAProps");
assert_eq!(binding.target_element, "button");
assert!(binding
.overridden_attributes
.contains(&"data-ouia-component-id".to_string()));
}
#[test]
fn test_function_component_pattern() {
let source = r#"
export const MyComponent = ({ testId, ...rest }: Props) => {
const testProps = generateTestIds('MyComponent', testId);
return <div {...rest} {...testProps} />;
};
"#;
let known = props(&["testId"]);
let data = data_attrs(&[("div", "data-testid")]);
let bindings = extract_managed_attributes(source, "MyComponent", &known, &data);
assert!(
!bindings.is_empty(),
"Expected managed attribute binding for testId"
);
assert_eq!(bindings[0].prop_name, "testId");
assert_eq!(bindings[0].generator_function, "generateTestIds");
}
#[test]
fn test_no_override_when_managed_before_rest() {
let source = r#"
export const MyComponent = ({ testId, ...rest }: Props) => {
const testProps = generateTestIds('MyComponent', testId);
return <div {...testProps} {...rest} />;
};
"#;
let known = props(&["testId"]);
let data = data_attrs(&[("div", "data-testid")]);
let bindings = extract_managed_attributes(source, "MyComponent", &known, &data);
assert!(
!bindings.is_empty(),
"Expected binding with consumer-wins order, got none"
);
assert!(
!bindings[0].component_overrides,
"Expected component_overrides=false when managed spread comes before rest"
);
}
#[test]
fn test_no_rest_no_override() {
let source = r#"
export const MyComponent = ({ testId }: Props) => {
const testProps = generateTestIds('MyComponent', testId);
return <div {...testProps} />;
};
"#;
let known = props(&["testId"]);
let data = data_attrs(&[("div", "data-testid")]);
let bindings = extract_managed_attributes(source, "MyComponent", &known, &data);
assert!(
bindings.is_empty(),
"Expected no bindings without rest parameter"
);
}
#[test]
fn test_inline_call_in_jsx_spread() {
let source = r#"
class Checkbox extends React.Component {
render() {
const { ouiaId, ouiaSafe, ...props } = this.props;
return (
<input
{...props}
{...getOUIAProps('Checkbox', ouiaId, ouiaSafe)}
/>
);
}
}
"#;
let known = props(&["ouiaId", "ouiaSafe"]);
let data = data_attrs(&[("input", "data-ouia-component-type")]);
let bindings = extract_managed_attributes(source, "Checkbox", &known, &data);
assert!(
!bindings.is_empty(),
"Expected managed attribute bindings for inline call, got none"
);
let binding = bindings.iter().find(|b| b.prop_name == "ouiaId");
assert!(
binding.is_some(),
"Expected ouiaId binding from inline call, found: {:?}",
bindings
);
assert_eq!(binding.unwrap().generator_function, "getOUIAProps");
assert_eq!(binding.unwrap().target_element, "input");
}
#[test]
fn test_inline_call_reverse_order_consumer_wins() {
let source = r#"
class Nav extends React.Component {
render() {
const { ouiaId, ouiaSafe, ...props } = this.props;
return (
<nav
{...getOUIAProps('Nav', ouiaId, ouiaSafe)}
{...props}
/>
);
}
}
"#;
let known = props(&["ouiaId", "ouiaSafe"]);
let data = data_attrs(&[("nav", "data-ouia-component-type")]);
let bindings = extract_managed_attributes(source, "Nav", &known, &data);
assert!(
!bindings.is_empty(),
"Expected binding with consumer-wins order, got none"
);
assert!(
!bindings[0].component_overrides,
"Expected component_overrides=false when inline call comes before rest"
);
assert_eq!(bindings[0].generator_function, "getOUIAProps");
}
#[test]
fn test_inline_call_with_ts_as_expression() {
let source = r#"
class Switch extends React.Component {
render() {
const { ouiaId, ouiaSafe, ...props } = this.props;
return (
<input
{...props}
{...(getOUIAProps('Switch', ouiaId, ouiaSafe) as any)}
/>
);
}
}
"#;
let known = props(&["ouiaId", "ouiaSafe"]);
let data = data_attrs(&[("input", "data-ouia-component-type")]);
let bindings = extract_managed_attributes(source, "Switch", &known, &data);
assert!(
!bindings.is_empty(),
"Expected bindings through TS `as` expression"
);
assert_eq!(bindings[0].generator_function, "getOUIAProps");
}
#[test]
fn test_inline_call_with_conditional() {
let source = r#"
export const MyComponent = ({ ouiaId, enabled, ...rest }: Props) => {
return (
<div
{...rest}
{...(enabled ? getOUIAProps('MyComponent', ouiaId) : {})}
/>
);
};
"#;
let known = props(&["ouiaId", "enabled"]);
let data = data_attrs(&[("div", "data-ouia-component-type")]);
let bindings = extract_managed_attributes(source, "MyComponent", &known, &data);
assert!(
!bindings.is_empty(),
"Expected bindings through conditional expression"
);
assert_eq!(bindings[0].generator_function, "getOUIAProps");
}
#[test]
fn test_inline_hook_call() {
let source = r#"
export const Alert = ({ ouiaId, ouiaSafe, ...rest }: AlertProps) => {
return (
<div
{...rest}
{...useOUIAProps('Alert', ouiaId, ouiaSafe)}
/>
);
};
"#;
let known = props(&["ouiaId", "ouiaSafe"]);
let data = data_attrs(&[("div", "data-ouia-component-type")]);
let bindings = extract_managed_attributes(source, "Alert", &known, &data);
assert!(
!bindings.is_empty(),
"Expected bindings from inline hook call"
);
assert_eq!(bindings[0].generator_function, "useOUIAProps");
}
#[test]
fn test_inline_method_call() {
let source = r#"
class Widget extends React.Component {
render() {
const { ouiaId, ...rest } = this.props;
return (
<div
{...rest}
{...this.getOUIAProps('Widget', ouiaId)}
/>
);
}
}
"#;
let known = props(&["ouiaId"]);
let data = data_attrs(&[("div", "data-ouia-component-type")]);
let bindings = extract_managed_attributes(source, "Widget", &known, &data);
assert!(
!bindings.is_empty(),
"Expected bindings from this.method() call"
);
assert_eq!(bindings[0].generator_function, "this.getOUIAProps");
}
#[test]
fn test_prop_with_nullish_coalescing() {
let source = r#"
class Base extends React.Component {
render() {
const { ouiaId, ouiaSafe, ...otherProps } = this.props;
const ouiaProps = getOUIAProps('X', ouiaId ?? this.state.id, ouiaSafe);
return <button {...otherProps} {...ouiaProps} />;
}
}
"#;
let known = props(&["ouiaId", "ouiaSafe"]);
let data = data_attrs(&[("button", "data-ouia-component-id")]);
let bindings = extract_managed_attributes(source, "X", &known, &data);
assert!(
!bindings.is_empty(),
"Should detect prop through ?? expression"
);
}
}