Files
millionco__react-doctor/native/oxlint/rules/r3f-valid-buffer-attribute-array-length.rs
2026-08-26 09:38:16 -07:00

115 lines
4.0 KiB
Rust

use oxc_ast::{ast::JSXAttributeValue, AstKind};
use oxc_diagnostics::OxcDiagnostic;
use oxc_span::GetSpan;
use crate::{
context::{ContextHost, LintContext},
rule::{Rule, RuleCategory, RuleInfo, RuleMeta},
};
const BUFFER_ATTRIBUTE_NAMES: [&str; 11] = [
"bufferAttribute",
"float16BufferAttribute",
"float32BufferAttribute",
"instancedBufferAttribute",
"int16BufferAttribute",
"int32BufferAttribute",
"int8BufferAttribute",
"uint16BufferAttribute",
"uint32BufferAttribute",
"uint8BufferAttribute",
"uint8ClampedBufferAttribute",
];
const ARRAY_ARGUMENT_INDEX: usize = 0;
const ITEM_SIZE_ARGUMENT_INDEX: usize = 1;
#[derive(Debug, Default, Clone)]
pub struct R3FValidBufferAttributeArrayLength;
impl RuleMeta for R3FValidBufferAttributeArrayLength {
const NAME: &'static str = "r3f-valid-buffer-attribute-array-length";
const PLUGIN: &'static str = "react_doctor_native";
const CATEGORY: RuleCategory = RuleCategory::Correctness;
const VERSION: &'static str = "0.1.0";
const INFO: RuleInfo = RuleInfo {
short_description: "Validate React Three Fiber buffer attribute array lengths.",
};
}
impl Rule for R3FValidBufferAttributeArrayLength {
fn should_run(&self, ctx: &ContextHost) -> bool {
ctx.source_type().is_jsx()
}
fn run_once<'a>(&self, ctx: &LintContext<'a>) {
if file_is_non_react_jsx_dialect(ctx) || !has_r3f_runtime_import(ctx) {
return;
}
for node in ctx.nodes().iter() {
let AstKind::JSXOpeningElement(opening_element) = node.kind() else {
continue;
};
if !matches!(
&opening_element.name,
oxc_ast::ast::JSXElementName::Identifier(identifier)
if BUFFER_ATTRIBUTE_NAMES.contains(&identifier.name.as_str())
) || opening_element.attributes.iter().any(|attribute| {
matches!(
attribute,
oxc_ast::ast::JSXAttributeItem::SpreadAttribute(_)
)
}) {
continue;
}
let Some(args_attribute) =
get_authoritative_jsx_attribute(opening_element, "args", true)
else {
continue;
};
let Some(JSXAttributeValue::ExpressionContainer(container)) = &args_attribute.value
else {
continue;
};
let Some(oxc_ast::ast::Expression::ArrayExpression(arguments)) = container
.expression
.as_expression()
.map(|expression| expression.get_inner_expression())
else {
continue;
};
let Some(array_expression) = arguments
.elements
.get(ARRAY_ARGUMENT_INDEX)
.and_then(oxc_ast::ast::ArrayExpressionElement::as_expression)
else {
continue;
};
let Some(item_size_expression) = arguments
.elements
.get(ITEM_SIZE_ARGUMENT_INDEX)
.and_then(oxc_ast::ast::ArrayExpressionElement::as_expression)
else {
continue;
};
let Some(array_length) = resolve_static_array_like_length(array_expression, ctx) else {
continue;
};
let Some(item_size) = resolve_static_number(item_size_expression, ctx) else {
continue;
};
if item_size.fract() != 0.0
|| item_size <= 0.0
|| array_length % item_size == 0.0
{
continue;
}
ctx.diagnostic(
OxcDiagnostic::error(format!(
"R3F BufferAttribute array length {array_length} is not divisible by itemSize {item_size}, so the final attribute item is incomplete"
))
.with_label(array_expression.span()),
);
}
}
}