use super::{Context, LintRule};
use crate::handler::{Handler, Traverse};
use crate::tags::{self, Tags};
use deno_ast::view::{
Expr, JSXAttrName, JSXAttrValue, JSXElementName, JSXExpr, Program,
};
use deno_ast::SourceRanged;
#[derive(Debug)]
pub struct FreshServerEventHandlers;
const CODE: &str = "fresh-server-event-handlers";
const MESSAGE: &str =
"Server components cannot install client side event handlers.";
const HINT: &str =
"Remove this property or turn the enclosing component into an island";
impl LintRule for FreshServerEventHandlers {
fn tags(&self) -> Tags {
&[tags::FRESH]
}
fn code(&self) -> &'static str {
CODE
}
fn lint_program_with_ast_view(
&self,
context: &mut Context,
program: Program,
) {
Visitor.traverse(program, context);
}
}
struct Visitor;
impl Handler for Visitor {
fn jsx_attr(
&mut self,
jsx_attr: &deno_ast::view::JSXAttr,
ctx: &mut Context,
) {
let Some(path_segments) = ctx.specifier().path_segments() else {
return;
};
let segments = path_segments.collect::<Vec<_>>();
if !segments.contains(&"routes") || segments.contains(&"(_islands)") {
return;
}
let parent_name = match jsx_attr.parent().name {
JSXElementName::Ident(name) => name.sym(),
_ => return,
};
let attr_name = match jsx_attr.name {
JSXAttrName::Ident(ident) => ident,
JSXAttrName::JSXNamespacedName(_) => return,
};
if *parent_name != parent_name.to_lowercase() {
return;
}
if parent_name.contains('-') {
let expr = match jsx_attr.value {
Some(JSXAttrValue::JSXExprContainer(expr)) => expr,
_ => return,
};
let JSXExpr::Expr(expr_value) = expr.expr else {
return;
};
match expr_value {
Expr::Arrow(_) => {
ctx.add_diagnostic_with_hint(jsx_attr.range(), CODE, MESSAGE, HINT);
}
Expr::Fn(_) => {
ctx.add_diagnostic_with_hint(jsx_attr.range(), CODE, MESSAGE, HINT);
}
_ => return,
}
return;
}
if !attr_name.sym().to_lowercase().starts_with("on") {
return;
}
if let Some(JSXAttrValue::JSXExprContainer(_)) = jsx_attr.value {
ctx.add_diagnostic_with_hint(jsx_attr.range(), CODE, MESSAGE, HINT);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_server_event_handler() {
assert_lint_ok!(
FreshServerEventHandlers,
filename: "file:///foo.jsx",
"<Foo onClick={() => {}} />",
);
assert_lint_ok!(
FreshServerEventHandlers,
filename: "file:///foo.jsx",
"<button onClick={() => {}} />",
);
assert_lint_ok!(
FreshServerEventHandlers,
filename: "file:///foo.jsx",
"<button onClick={function () {}} />",
);
assert_lint_ok!(
FreshServerEventHandlers,
filename: "file:///foo.jsx",
"<button onclick={function () {}} />",
);
assert_lint_ok!(
FreshServerEventHandlers,
filename: "file:///foo.jsx",
"<button onClick=\"console.log('hey')\" />",
);
assert_lint_ok!(
FreshServerEventHandlers,
filename: "file:///foo.jsx",
"<button online=\"foo\" />",
);
assert_lint_ok!(
FreshServerEventHandlers,
filename: "file:///foo.jsx",
"<x-foo onClick=\"console.log('hey')\" />",
);
assert_lint_ok!(
FreshServerEventHandlers,
filename: "file:///routes/foo/(_islands)/foo.jsx",
"<button onClick={function () {}} />",
);
assert_lint_err!(FreshServerEventHandlers, filename: "file:///routes/index.tsx", r#"<button onClick={() => {}} />"#: [
{
col: 8,
message: MESSAGE,
hint: HINT,
}]);
assert_lint_err!(FreshServerEventHandlers, filename: "file:///routes/index.tsx", r#"<button onTouchMove={() => {}} />"#: [
{
col: 8,
message: MESSAGE,
hint: HINT,
}]);
assert_lint_err!(FreshServerEventHandlers, filename: "file:///routes/index.tsx", r#"<button onTouchMove={"console.log('hey')"} />"#: [
{
col: 8,
message: MESSAGE,
hint: HINT,
}]);
assert_lint_err!(FreshServerEventHandlers, filename: "file:///routes/index.tsx", r#"<foo-button foo={() => {}} />"#: [
{
col: 12,
message: MESSAGE,
hint: HINT,
}]);
assert_lint_err!(FreshServerEventHandlers, filename: "file:///routes/index.tsx", r#"<foo-button foo={function () {}} />"#: [
{
col: 12,
message: MESSAGE,
hint: HINT,
}]);
}
}