mirror of
https://github.com/millionco/react-doctor.git
synced 2026-09-14 20:00:24 +08:00
801 lines
28 KiB
Rust
801 lines
28 KiB
Rust
use oxc_ast::{
|
|
AstKind,
|
|
ast::{
|
|
Expression, FunctionType, JSXElementName, JSXMemberExpression, JSXMemberExpressionObject,
|
|
MemberExpression,
|
|
},
|
|
};
|
|
use oxc_diagnostics::OxcDiagnostic;
|
|
use oxc_semantic::{NodeId, SymbolId};
|
|
use oxc_span::GetSpan;
|
|
use rustc_hash::{FxHashMap, FxHashSet};
|
|
|
|
use crate::{
|
|
context::{ContextHost, LintContext},
|
|
rule::{Rule, RuleCategory, RuleInfo, RuleMeta},
|
|
};
|
|
|
|
const LEGACY_SHADER_MATERIAL_HOST_NAMES: [&str; 2] = ["rawShaderMaterial", "shaderMaterial"];
|
|
const LEGACY_MATERIAL_MESSAGE: &str = "ShaderMaterial and RawShaderMaterial do not run on Three.js WebGPURenderer. Build this shader with a node material and TSL";
|
|
const ON_BEFORE_COMPILE_MESSAGE: &str = "onBeforeCompile patches WebGL shader source and is not supported by Three.js WebGPURenderer. Use a node material and TSL";
|
|
const R3F_PUBLIC_MODULES: [&str; 5] = [
|
|
"@react-three/fiber",
|
|
"@react-three/fiber/legacy",
|
|
"@react-three/fiber/native",
|
|
"@react-three/fiber/webgpu",
|
|
"react-three-fiber",
|
|
];
|
|
const THREE_WEBGPU_MODULES: [&str; 2] = ["three", "three/"];
|
|
|
|
#[derive(Debug, Default, Clone)]
|
|
pub struct R3FWebgpuNoLegacyMaterialApi;
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
enum R3fLegacyMaterialHostKind {
|
|
LegacyShaderMaterial,
|
|
Material,
|
|
Other,
|
|
}
|
|
|
|
impl RuleMeta for R3FWebgpuNoLegacyMaterialApi {
|
|
const NAME: &'static str = "r3f-webgpu-no-legacy-material-api";
|
|
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: "Disallow legacy material APIs in R3F WebGPU roots.",
|
|
};
|
|
}
|
|
|
|
impl Rule for R3FWebgpuNoLegacyMaterialApi {
|
|
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) {
|
|
return;
|
|
}
|
|
|
|
let candidate_ids = ctx
|
|
.nodes()
|
|
.iter()
|
|
.filter_map(|node| {
|
|
let AstKind::JSXOpeningElement(opening_element) = node.kind() else {
|
|
return None;
|
|
};
|
|
let host_kind = r3f_legacy_material_host_kind(opening_element, ctx)?;
|
|
(host_kind == R3fLegacyMaterialHostKind::LegacyShaderMaterial
|
|
|| (host_kind == R3fLegacyMaterialHostKind::Material
|
|
&& get_authoritative_jsx_attribute(
|
|
opening_element,
|
|
"onBeforeCompile",
|
|
true,
|
|
)
|
|
.is_some()))
|
|
.then_some(node.id())
|
|
})
|
|
.collect::<Vec<_>>();
|
|
if candidate_ids.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let analysis = build_possible_static_property_write_analysis(ctx);
|
|
let node_index = build_local_callback_nearest_function_node_index(ctx);
|
|
let mut resolution_cache = LocalFunctionResolutionCache::default();
|
|
let mut assigned_expression_cache = R3fAnalyzedAssignedExpressionCache::default();
|
|
let mut webgpu_canvas_cache = FxHashMap::default();
|
|
for candidate_id in candidate_ids {
|
|
if !r3f_legacy_material_is_inside_webgpu_canvas(
|
|
candidate_id,
|
|
&analysis,
|
|
&node_index,
|
|
ctx,
|
|
&mut resolution_cache,
|
|
&mut assigned_expression_cache,
|
|
&mut webgpu_canvas_cache,
|
|
) && !r3f_legacy_material_is_inside_local_webgpu_component(
|
|
candidate_id,
|
|
&analysis,
|
|
&node_index,
|
|
ctx,
|
|
&mut resolution_cache,
|
|
&mut assigned_expression_cache,
|
|
&mut webgpu_canvas_cache,
|
|
) {
|
|
continue;
|
|
}
|
|
let candidate = ctx.nodes().get_node(candidate_id);
|
|
let AstKind::JSXOpeningElement(opening_element) = candidate.kind() else {
|
|
continue;
|
|
};
|
|
let Some(host_kind) = r3f_legacy_material_host_kind(opening_element, ctx) else {
|
|
continue;
|
|
};
|
|
if host_kind == R3fLegacyMaterialHostKind::LegacyShaderMaterial {
|
|
ctx.diagnostic(
|
|
OxcDiagnostic::error(LEGACY_MATERIAL_MESSAGE).with_label(candidate.span()),
|
|
);
|
|
continue;
|
|
}
|
|
if let Some(attribute) =
|
|
get_authoritative_jsx_attribute(opening_element, "onBeforeCompile", true)
|
|
{
|
|
ctx.diagnostic(
|
|
OxcDiagnostic::error(ON_BEFORE_COMPILE_MESSAGE).with_label(attribute.span),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn r3f_legacy_material_host_kind<'a>(
|
|
opening_element: &oxc_ast::ast::JSXOpeningElement<'a>,
|
|
ctx: &LintContext<'a>,
|
|
) -> Option<R3fLegacyMaterialHostKind> {
|
|
let element_type = match &opening_element.name {
|
|
JSXElementName::Identifier(identifier) => {
|
|
let element_type = resolve_jsx_element_type(opening_element, ctx)
|
|
.map_or(identifier.name.as_str(), |(element_type, _)| element_type);
|
|
if !r3f_legacy_material_is_host_intrinsic_name(element_type, opening_element, ctx) {
|
|
return None;
|
|
}
|
|
return Some(r3f_legacy_material_classify_host_name(element_type));
|
|
}
|
|
JSXElementName::IdentifierReference(identifier) => {
|
|
let element_type = resolve_jsx_element_type(opening_element, ctx)
|
|
.map_or(identifier.name.as_str(), |(element_type, _)| element_type);
|
|
if !r3f_legacy_material_is_host_intrinsic_name(element_type, opening_element, ctx) {
|
|
return None;
|
|
}
|
|
return Some(r3f_legacy_material_classify_host_name(element_type));
|
|
}
|
|
JSXElementName::MemberExpression(member_expression) => {
|
|
if !r3f_legacy_material_member_name_is_host_intrinsic(member_expression) {
|
|
return None;
|
|
}
|
|
member_expression.property.name.as_str()
|
|
}
|
|
JSXElementName::NamespacedName(namespaced_name) => {
|
|
let namespace = namespaced_name.namespace.name.as_str();
|
|
let name = namespaced_name.name.name.as_str();
|
|
if !r3f_legacy_material_has_lowercase_initial(namespace)
|
|
|| namespace.contains('-')
|
|
|| name.contains('-')
|
|
{
|
|
return None;
|
|
}
|
|
name
|
|
}
|
|
JSXElementName::ThisExpression(_) => return None,
|
|
};
|
|
Some(if element_type.ends_with("Material") {
|
|
R3fLegacyMaterialHostKind::Material
|
|
} else {
|
|
R3fLegacyMaterialHostKind::Other
|
|
})
|
|
}
|
|
|
|
fn r3f_legacy_material_classify_host_name(element_type: &str) -> R3fLegacyMaterialHostKind {
|
|
if LEGACY_SHADER_MATERIAL_HOST_NAMES.contains(&element_type) {
|
|
R3fLegacyMaterialHostKind::LegacyShaderMaterial
|
|
} else if element_type.ends_with("Material") {
|
|
R3fLegacyMaterialHostKind::Material
|
|
} else {
|
|
R3fLegacyMaterialHostKind::Other
|
|
}
|
|
}
|
|
|
|
fn r3f_legacy_material_is_host_intrinsic_name(
|
|
element_type: &str,
|
|
opening_element: &oxc_ast::ast::JSXOpeningElement<'_>,
|
|
ctx: &LintContext<'_>,
|
|
) -> bool {
|
|
r3f_legacy_material_has_lowercase_initial(element_type)
|
|
&& !element_type.contains('-')
|
|
&& !crate::globals::HTML_TAG.contains(element_type)
|
|
&& (!is_svg_tag_name(element_type)
|
|
|| (element_type == "line"
|
|
&& !ctx
|
|
.nodes()
|
|
.ancestors(opening_element.node_id.get())
|
|
.any(|ancestor| {
|
|
matches!(
|
|
ancestor.kind(),
|
|
AstKind::JSXElement(element)
|
|
if matches!(
|
|
&element.opening_element.name,
|
|
JSXElementName::Identifier(identifier) if identifier.name == "svg"
|
|
)
|
|
)
|
|
})))
|
|
}
|
|
|
|
fn r3f_legacy_material_member_name_is_host_intrinsic(
|
|
member_expression: &JSXMemberExpression<'_>,
|
|
) -> bool {
|
|
let root_name = r3f_legacy_material_member_root_name(member_expression);
|
|
r3f_legacy_material_has_lowercase_initial(root_name)
|
|
&& !r3f_legacy_material_member_name_contains_hyphen(member_expression)
|
|
}
|
|
|
|
fn r3f_legacy_material_member_root_name<'a, 'node>(
|
|
member_expression: &'node JSXMemberExpression<'a>,
|
|
) -> &'node str {
|
|
match &member_expression.object {
|
|
JSXMemberExpressionObject::IdentifierReference(identifier) => identifier.name.as_str(),
|
|
JSXMemberExpressionObject::MemberExpression(parent_member) => {
|
|
r3f_legacy_material_member_root_name(parent_member)
|
|
}
|
|
JSXMemberExpressionObject::ThisExpression(_) => "this",
|
|
}
|
|
}
|
|
|
|
fn r3f_legacy_material_member_name_contains_hyphen(
|
|
member_expression: &JSXMemberExpression<'_>,
|
|
) -> bool {
|
|
member_expression.property.name.contains('-')
|
|
|| match &member_expression.object {
|
|
JSXMemberExpressionObject::IdentifierReference(identifier) => {
|
|
identifier.name.contains('-')
|
|
}
|
|
JSXMemberExpressionObject::MemberExpression(parent_member) => {
|
|
r3f_legacy_material_member_name_contains_hyphen(parent_member)
|
|
}
|
|
JSXMemberExpressionObject::ThisExpression(_) => false,
|
|
}
|
|
}
|
|
|
|
fn r3f_legacy_material_has_lowercase_initial(name: &str) -> bool {
|
|
name.chars()
|
|
.next()
|
|
.is_some_and(|character| character.to_lowercase().eq(std::iter::once(character)))
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn r3f_legacy_material_is_inside_webgpu_canvas<'a>(
|
|
node_id: NodeId,
|
|
analysis: &PossibleStaticPropertyWriteAnalysis,
|
|
node_index: &LocalCallbackNearestFunctionNodeIndex,
|
|
ctx: &LintContext<'a>,
|
|
resolution_cache: &mut LocalFunctionResolutionCache,
|
|
assigned_expression_cache: &mut R3fAnalyzedAssignedExpressionCache<'a>,
|
|
webgpu_canvas_cache: &mut FxHashMap<NodeId, bool>,
|
|
) -> bool {
|
|
for ancestor in ctx.nodes().ancestors(node_id) {
|
|
let AstKind::JSXElement(canvas) = ancestor.kind() else {
|
|
continue;
|
|
};
|
|
let Some(canvas_module) =
|
|
r3f_legacy_material_canvas_module(&canvas.opening_element.name, analysis, ctx)
|
|
else {
|
|
continue;
|
|
};
|
|
if let Some(&is_webgpu) = webgpu_canvas_cache.get(&ancestor.id()) {
|
|
return is_webgpu;
|
|
}
|
|
let is_webgpu = canvas_module == "@react-three/fiber/webgpu"
|
|
|| r3f_legacy_material_canvas_creates_webgpu_renderer(
|
|
canvas,
|
|
analysis,
|
|
node_index,
|
|
ctx,
|
|
resolution_cache,
|
|
assigned_expression_cache,
|
|
);
|
|
webgpu_canvas_cache.insert(ancestor.id(), is_webgpu);
|
|
return is_webgpu;
|
|
}
|
|
false
|
|
}
|
|
|
|
fn r3f_legacy_material_canvas_module<'a>(
|
|
element_name: &JSXElementName<'a>,
|
|
analysis: &PossibleStaticPropertyWriteAnalysis,
|
|
ctx: &LintContext<'a>,
|
|
) -> Option<&'static str> {
|
|
R3F_PUBLIC_MODULES.into_iter().find(|module_source| {
|
|
jsx_module_api_reference_matches(element_name, "Canvas", &[*module_source], analysis, ctx)
|
|
})
|
|
}
|
|
|
|
fn r3f_legacy_material_canvas_creates_webgpu_renderer<'a>(
|
|
canvas: &oxc_ast::ast::JSXElement<'a>,
|
|
analysis: &PossibleStaticPropertyWriteAnalysis,
|
|
node_index: &LocalCallbackNearestFunctionNodeIndex,
|
|
ctx: &LintContext<'a>,
|
|
resolution_cache: &mut LocalFunctionResolutionCache,
|
|
assigned_expression_cache: &mut R3fAnalyzedAssignedExpressionCache<'a>,
|
|
) -> bool {
|
|
let Some(factory_expression) =
|
|
get_authoritative_jsx_attribute(&canvas.opening_element, "gl", true)
|
|
.and_then(jsx_attribute_expression)
|
|
else {
|
|
return false;
|
|
};
|
|
if r3f_legacy_material_is_webgpu_renderer(factory_expression, analysis, ctx) {
|
|
return true;
|
|
}
|
|
let Some(factory_id) = resolve_r3f_analyzed_callback_function_id(
|
|
factory_expression,
|
|
analysis,
|
|
ctx,
|
|
resolution_cache,
|
|
) else {
|
|
return false;
|
|
};
|
|
r3f_legacy_material_function_returns_webgpu_renderer(
|
|
factory_id,
|
|
analysis,
|
|
node_index,
|
|
ctx,
|
|
resolution_cache,
|
|
assigned_expression_cache,
|
|
&mut Vec::new(),
|
|
)
|
|
}
|
|
|
|
fn r3f_legacy_material_is_webgpu_renderer<'a>(
|
|
expression: &Expression<'a>,
|
|
analysis: &PossibleStaticPropertyWriteAnalysis,
|
|
ctx: &LintContext<'a>,
|
|
) -> bool {
|
|
let Expression::NewExpression(constructor) = expression.get_inner_expression() else {
|
|
return false;
|
|
};
|
|
module_api_reference_matches(
|
|
&constructor.callee,
|
|
"WebGPURenderer",
|
|
&THREE_WEBGPU_MODULES,
|
|
analysis,
|
|
ctx,
|
|
) || type_import_module_api_reference_matches(
|
|
&constructor.callee,
|
|
"WebGPURenderer",
|
|
&THREE_WEBGPU_MODULES,
|
|
analysis,
|
|
ctx,
|
|
)
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn r3f_legacy_material_function_returns_webgpu_renderer<'a>(
|
|
function_id: NodeId,
|
|
analysis: &PossibleStaticPropertyWriteAnalysis,
|
|
node_index: &LocalCallbackNearestFunctionNodeIndex,
|
|
ctx: &LintContext<'a>,
|
|
resolution_cache: &mut LocalFunctionResolutionCache,
|
|
assigned_expression_cache: &mut R3fAnalyzedAssignedExpressionCache<'a>,
|
|
visited_function_ids: &mut Vec<NodeId>,
|
|
) -> bool {
|
|
if visited_function_ids.contains(&function_id) {
|
|
return false;
|
|
}
|
|
visited_function_ids.push(function_id);
|
|
if let AstKind::ArrowFunctionExpression(function) = ctx.nodes().get_node(function_id).kind()
|
|
&& let Some(expression) = function.get_expression()
|
|
{
|
|
let matches = r3f_legacy_material_return_is_webgpu_renderer(
|
|
expression,
|
|
analysis,
|
|
node_index,
|
|
ctx,
|
|
resolution_cache,
|
|
assigned_expression_cache,
|
|
visited_function_ids,
|
|
&mut Vec::new(),
|
|
);
|
|
visited_function_ids.pop();
|
|
return matches;
|
|
}
|
|
let body_statements = match ctx.nodes().get_node(function_id).kind() {
|
|
AstKind::Function(function) => function
|
|
.body
|
|
.as_ref()
|
|
.map(|body| body.statements.as_slice()),
|
|
AstKind::ArrowFunctionExpression(function) => function
|
|
.body
|
|
.as_function_body()
|
|
.map(|body| body.statements.as_slice()),
|
|
_ => None,
|
|
};
|
|
let Some(body_statements) = body_statements else {
|
|
visited_function_ids.pop();
|
|
return false;
|
|
};
|
|
let mut returned_expressions = Vec::new();
|
|
let mut has_bare_return = false;
|
|
for &candidate_id in node_index.node_ids(function_id) {
|
|
let AstKind::ReturnStatement(statement) = ctx.nodes().get_node(candidate_id).kind() else {
|
|
continue;
|
|
};
|
|
if let Some(argument) = statement.argument.as_ref() {
|
|
returned_expressions.push(argument);
|
|
} else {
|
|
has_bare_return = true;
|
|
}
|
|
}
|
|
let matches = !has_bare_return
|
|
&& body_statements.iter().any(statement_always_exits)
|
|
&& !returned_expressions.is_empty()
|
|
&& returned_expressions.into_iter().all(|expression| {
|
|
r3f_legacy_material_return_is_webgpu_renderer(
|
|
expression,
|
|
analysis,
|
|
node_index,
|
|
ctx,
|
|
resolution_cache,
|
|
assigned_expression_cache,
|
|
visited_function_ids,
|
|
&mut Vec::new(),
|
|
)
|
|
});
|
|
visited_function_ids.pop();
|
|
matches
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn r3f_legacy_material_return_is_webgpu_renderer<'a>(
|
|
expression: &'a Expression<'a>,
|
|
analysis: &PossibleStaticPropertyWriteAnalysis,
|
|
node_index: &LocalCallbackNearestFunctionNodeIndex,
|
|
ctx: &LintContext<'a>,
|
|
resolution_cache: &mut LocalFunctionResolutionCache,
|
|
assigned_expression_cache: &mut R3fAnalyzedAssignedExpressionCache<'a>,
|
|
visited_function_ids: &mut Vec<NodeId>,
|
|
visited_symbol_ids: &mut Vec<SymbolId>,
|
|
) -> bool {
|
|
let expression = expression.get_inner_expression();
|
|
if r3f_legacy_material_is_webgpu_renderer(expression, analysis, ctx) {
|
|
return true;
|
|
}
|
|
match expression {
|
|
Expression::Identifier(identifier) => {
|
|
let Some(symbol_id) = ctx
|
|
.scoping()
|
|
.get_reference(identifier.reference_id())
|
|
.symbol_id()
|
|
else {
|
|
return false;
|
|
};
|
|
if visited_symbol_ids.contains(&symbol_id) {
|
|
return false;
|
|
}
|
|
visited_symbol_ids.push(symbol_id);
|
|
let assigned_expressions = r3f_analyzed_possible_assigned_expressions(
|
|
identifier,
|
|
symbol_id,
|
|
ctx,
|
|
assigned_expression_cache,
|
|
);
|
|
let matches = !assigned_expressions.is_empty()
|
|
&& assigned_expressions.into_iter().all(|assigned_expression| {
|
|
!matches!(
|
|
assigned_expression.get_inner_expression(),
|
|
Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_)
|
|
) && r3f_legacy_material_return_is_webgpu_renderer(
|
|
assigned_expression,
|
|
analysis,
|
|
node_index,
|
|
ctx,
|
|
resolution_cache,
|
|
assigned_expression_cache,
|
|
visited_function_ids,
|
|
&mut visited_symbol_ids.clone(),
|
|
)
|
|
});
|
|
visited_symbol_ids.pop();
|
|
matches
|
|
}
|
|
Expression::CallExpression(call_expression)
|
|
if call_expression.arguments.is_empty()
|
|
&& matches!(&call_expression.callee, Expression::Identifier(_)) =>
|
|
{
|
|
r3f_legacy_material_zero_argument_function_id(call_expression, ctx).is_some_and(
|
|
|called_function_id| {
|
|
r3f_legacy_material_function_returns_webgpu_renderer(
|
|
called_function_id,
|
|
analysis,
|
|
node_index,
|
|
ctx,
|
|
resolution_cache,
|
|
assigned_expression_cache,
|
|
visited_function_ids,
|
|
)
|
|
},
|
|
)
|
|
}
|
|
Expression::ConditionalExpression(conditional) => {
|
|
r3f_legacy_material_return_is_webgpu_renderer(
|
|
&conditional.consequent,
|
|
analysis,
|
|
node_index,
|
|
ctx,
|
|
resolution_cache,
|
|
assigned_expression_cache,
|
|
visited_function_ids,
|
|
&mut visited_symbol_ids.clone(),
|
|
) && r3f_legacy_material_return_is_webgpu_renderer(
|
|
&conditional.alternate,
|
|
analysis,
|
|
node_index,
|
|
ctx,
|
|
resolution_cache,
|
|
assigned_expression_cache,
|
|
visited_function_ids,
|
|
&mut visited_symbol_ids.clone(),
|
|
)
|
|
}
|
|
Expression::LogicalExpression(logical) => {
|
|
r3f_legacy_material_return_is_webgpu_renderer(
|
|
&logical.left,
|
|
analysis,
|
|
node_index,
|
|
ctx,
|
|
resolution_cache,
|
|
assigned_expression_cache,
|
|
visited_function_ids,
|
|
&mut visited_symbol_ids.clone(),
|
|
) && r3f_legacy_material_return_is_webgpu_renderer(
|
|
&logical.right,
|
|
analysis,
|
|
node_index,
|
|
ctx,
|
|
resolution_cache,
|
|
assigned_expression_cache,
|
|
visited_function_ids,
|
|
&mut visited_symbol_ids.clone(),
|
|
)
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
fn r3f_legacy_material_zero_argument_function_id(
|
|
call_expression: &oxc_ast::ast::CallExpression<'_>,
|
|
ctx: &LintContext<'_>,
|
|
) -> Option<NodeId> {
|
|
let Expression::Identifier(identifier) = &call_expression.callee else {
|
|
return None;
|
|
};
|
|
let symbol_id = ctx
|
|
.scoping()
|
|
.get_reference(identifier.reference_id())
|
|
.symbol_id()?;
|
|
let declaration = ctx.symbol_declaration(symbol_id);
|
|
match declaration.kind() {
|
|
AstKind::Function(function) => {
|
|
(!function.r#async && !function.generator && function.params.items.is_empty())
|
|
.then_some(declaration.id())
|
|
}
|
|
AstKind::VariableDeclarator(declarator) => {
|
|
if !matches!(
|
|
ctx.nodes().parent_node(declaration.id()).kind(),
|
|
AstKind::VariableDeclaration(variable_declaration)
|
|
if variable_declaration.kind.is_const()
|
|
) {
|
|
return None;
|
|
}
|
|
let initializer = declarator.init.as_ref()?.get_inner_expression();
|
|
match initializer {
|
|
Expression::ArrowFunctionExpression(function)
|
|
if !function.r#async && function.params.items.is_empty() =>
|
|
{
|
|
Some(function.node_id.get())
|
|
}
|
|
Expression::FunctionExpression(function)
|
|
if !function.r#async
|
|
&& !function.generator
|
|
&& function.params.items.is_empty() =>
|
|
{
|
|
Some(function.node_id.get())
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn r3f_legacy_material_is_inside_local_webgpu_component<'a>(
|
|
node_id: NodeId,
|
|
analysis: &PossibleStaticPropertyWriteAnalysis,
|
|
node_index: &LocalCallbackNearestFunctionNodeIndex,
|
|
ctx: &LintContext<'a>,
|
|
resolution_cache: &mut LocalFunctionResolutionCache,
|
|
assigned_expression_cache: &mut R3fAnalyzedAssignedExpressionCache<'a>,
|
|
webgpu_canvas_cache: &mut FxHashMap<NodeId, bool>,
|
|
) -> bool {
|
|
let Some(component_function_id) = local_callback_nearest_function_id(node_id, ctx) else {
|
|
return false;
|
|
};
|
|
let Some(component_symbol_id) =
|
|
r3f_legacy_material_local_component_symbol(component_function_id, ctx)
|
|
else {
|
|
return false;
|
|
};
|
|
r3f_legacy_material_component_is_rendered_inside_webgpu_canvas(
|
|
component_symbol_id,
|
|
analysis,
|
|
node_index,
|
|
ctx,
|
|
resolution_cache,
|
|
assigned_expression_cache,
|
|
webgpu_canvas_cache,
|
|
&mut FxHashSet::default(),
|
|
)
|
|
}
|
|
|
|
fn r3f_legacy_material_local_component_symbol(
|
|
function_id: NodeId,
|
|
ctx: &LintContext<'_>,
|
|
) -> Option<SymbolId> {
|
|
let function_node = ctx.nodes().get_node(function_id);
|
|
if let AstKind::Function(function) = function_node.kind()
|
|
&& function.r#type == FunctionType::FunctionDeclaration
|
|
{
|
|
return function
|
|
.id
|
|
.as_ref()
|
|
.map(|identifier| identifier.symbol_id());
|
|
}
|
|
let mut expression_root = transparent_expression_root(function_node, ctx);
|
|
loop {
|
|
let parent = ctx.nodes().parent_node(expression_root.id());
|
|
let AstKind::CallExpression(call_expression) = parent.kind() else {
|
|
break;
|
|
};
|
|
if !call_expression.arguments.first().is_some_and(|argument| {
|
|
argument
|
|
.as_expression()
|
|
.is_some_and(|expression| expression.span() == expression_root.span())
|
|
}) || !r3f_legacy_material_is_react_hoc_call(call_expression)
|
|
{
|
|
break;
|
|
}
|
|
expression_root = transparent_expression_root(parent, ctx);
|
|
}
|
|
let declaration = ctx.nodes().parent_node(expression_root.id());
|
|
let AstKind::VariableDeclarator(declarator) = declaration.kind() else {
|
|
return None;
|
|
};
|
|
if !matches!(
|
|
ctx.nodes().parent_node(declaration.id()).kind(),
|
|
AstKind::VariableDeclaration(variable_declaration)
|
|
if variable_declaration.kind.is_const()
|
|
) || declarator
|
|
.init
|
|
.as_ref()
|
|
.is_none_or(|initializer| initializer.span() != expression_root.span())
|
|
{
|
|
return None;
|
|
}
|
|
declarator
|
|
.id
|
|
.get_binding_identifier()
|
|
.map(|identifier| identifier.symbol_id())
|
|
}
|
|
|
|
fn r3f_legacy_material_is_react_hoc_call(
|
|
call_expression: &oxc_ast::ast::CallExpression<'_>,
|
|
) -> bool {
|
|
match call_expression.callee.get_inner_expression() {
|
|
Expression::Identifier(identifier) => {
|
|
matches!(identifier.name.as_str(), "memo" | "forwardRef")
|
|
}
|
|
expression => expression.as_member_expression().is_some_and(|member| {
|
|
let MemberExpression::StaticMemberExpression(member) = member else {
|
|
return false;
|
|
};
|
|
matches!(member.property.name.as_str(), "memo" | "forwardRef")
|
|
&& matches!(
|
|
member.object.get_inner_expression(),
|
|
Expression::Identifier(identifier) if identifier.name == "React"
|
|
)
|
|
}),
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn r3f_legacy_material_component_is_rendered_inside_webgpu_canvas<'a>(
|
|
component_symbol_id: SymbolId,
|
|
analysis: &PossibleStaticPropertyWriteAnalysis,
|
|
node_index: &LocalCallbackNearestFunctionNodeIndex,
|
|
ctx: &LintContext<'a>,
|
|
resolution_cache: &mut LocalFunctionResolutionCache,
|
|
assigned_expression_cache: &mut R3fAnalyzedAssignedExpressionCache<'a>,
|
|
webgpu_canvas_cache: &mut FxHashMap<NodeId, bool>,
|
|
visited_symbol_ids: &mut FxHashSet<SymbolId>,
|
|
) -> bool {
|
|
if visited_symbol_ids.contains(&component_symbol_id)
|
|
|| ctx
|
|
.scoping()
|
|
.get_resolved_references(component_symbol_id)
|
|
.any(|reference| !reference.is_read() || reference.is_write())
|
|
{
|
|
return false;
|
|
}
|
|
visited_symbol_ids.insert(component_symbol_id);
|
|
let reference_node_ids = ctx
|
|
.scoping()
|
|
.get_resolved_references(component_symbol_id)
|
|
.map(|reference| reference.node_id())
|
|
.collect::<Vec<_>>();
|
|
for reference_node_id in reference_node_ids {
|
|
let reference_node = ctx.nodes().get_node(reference_node_id);
|
|
let opening_element_node = ctx.nodes().parent_node(reference_node.id());
|
|
let AstKind::JSXOpeningElement(opening_element) = opening_element_node.kind() else {
|
|
continue;
|
|
};
|
|
if !matches!(
|
|
&opening_element.name,
|
|
JSXElementName::IdentifierReference(identifier)
|
|
if identifier.node_id.get() == reference_node_id
|
|
) {
|
|
continue;
|
|
}
|
|
if r3f_legacy_material_is_inside_webgpu_canvas(
|
|
opening_element_node.id(),
|
|
analysis,
|
|
node_index,
|
|
ctx,
|
|
resolution_cache,
|
|
assigned_expression_cache,
|
|
webgpu_canvas_cache,
|
|
) {
|
|
visited_symbol_ids.remove(&component_symbol_id);
|
|
return true;
|
|
}
|
|
let Some(enclosing_component_id) = r3f_legacy_material_enclosing_component_without_canvas(
|
|
opening_element_node.id(),
|
|
analysis,
|
|
ctx,
|
|
) else {
|
|
continue;
|
|
};
|
|
let Some(enclosing_component_symbol_id) =
|
|
r3f_legacy_material_local_component_symbol(enclosing_component_id, ctx)
|
|
else {
|
|
continue;
|
|
};
|
|
if r3f_legacy_material_component_is_rendered_inside_webgpu_canvas(
|
|
enclosing_component_symbol_id,
|
|
analysis,
|
|
node_index,
|
|
ctx,
|
|
resolution_cache,
|
|
assigned_expression_cache,
|
|
webgpu_canvas_cache,
|
|
visited_symbol_ids,
|
|
) {
|
|
visited_symbol_ids.remove(&component_symbol_id);
|
|
return true;
|
|
}
|
|
}
|
|
visited_symbol_ids.remove(&component_symbol_id);
|
|
false
|
|
}
|
|
|
|
fn r3f_legacy_material_enclosing_component_without_canvas(
|
|
node_id: NodeId,
|
|
analysis: &PossibleStaticPropertyWriteAnalysis,
|
|
ctx: &LintContext<'_>,
|
|
) -> Option<NodeId> {
|
|
for ancestor in ctx.nodes().ancestors(node_id) {
|
|
if let AstKind::JSXElement(element) = ancestor.kind()
|
|
&& r3f_legacy_material_canvas_module(&element.opening_element.name, analysis, ctx)
|
|
.is_some()
|
|
{
|
|
return None;
|
|
}
|
|
if matches!(
|
|
ancestor.kind(),
|
|
AstKind::Function(_) | AstKind::ArrowFunctionExpression(_)
|
|
) {
|
|
return Some(ancestor.id());
|
|
}
|
|
}
|
|
None
|
|
}
|