use super::{Context, LintRule};
use crate::handler::{Handler, Traverse};
use crate::tags::{self, Tags};
use crate::Program;
use deno_ast::view::{
ArrayLit, BlockStmtOrExpr, CallExpr, Callee, Expr, JSXAttrName,
JSXAttrOrSpread, MemberProp, OptCall, OptChainBase, Stmt,
};
use deno_ast::SourceRanged;
#[derive(Debug)]
pub struct JSXKey;
const CODE: &str = "jsx-key";
impl LintRule for JSXKey {
fn tags(&self) -> Tags {
&[tags::RECOMMENDED, tags::REACT, tags::JSX]
}
fn code(&self) -> &'static str {
CODE
}
fn lint_program_with_ast_view(
&self,
context: &mut Context,
program: Program,
) {
JSXKeyHandler.traverse(program, context);
}
}
enum DiagnosticKind {
MissingKey,
MissingFragKey,
}
impl DiagnosticKind {
fn message(&self) -> &'static str {
match *self {
DiagnosticKind::MissingKey => "Missing 'key' prop for an element",
DiagnosticKind::MissingFragKey => "Missing 'key' prop for a Fragment",
}
}
fn hint(&self) -> &'static str {
match *self {
DiagnosticKind::MissingKey => "Add a 'key' prop",
DiagnosticKind::MissingFragKey => {
"Use the `<Fragment key=\"..\">` syntax instead"
}
}
}
}
struct JSXKeyHandler;
impl Handler for JSXKeyHandler {
fn array_lit(&mut self, node: &ArrayLit, ctx: &mut Context) {
for elem in node.elems.iter().flatten() {
check_expr(ctx, &elem.expr);
}
}
fn opt_call(&mut self, node: &OptCall, ctx: &mut Context) {
if is_map_member(&node.callee) {
if let Some(callback) = node.args.first() {
match callback.expr {
Expr::Arrow(arrow_fn) => match arrow_fn.body {
BlockStmtOrExpr::BlockStmt(stmt) => {
check_stmt(ctx, &Stmt::Block(stmt))
}
BlockStmtOrExpr::Expr(expr) => check_expr(ctx, &expr),
},
Expr::Fn(fn_expr) => {
if let Some(body) = fn_expr.function.body {
check_stmt(ctx, &Stmt::Block(body))
}
}
_ => {}
}
}
}
}
fn call_expr(&mut self, node: &CallExpr, ctx: &mut Context) {
if let Callee::Expr(callee) = node.callee {
if is_map_member(&callee) {
if let Some(callback) = node.args.first() {
check_callback(ctx, &callback.expr)
}
} else if let Expr::Member(member) = callee {
let Expr::Ident(id) = member.obj else {
return;
};
let MemberProp::Ident(member_id) = member.prop else {
return;
};
if id.sym() == "Array" && member_id.sym() == "from" {
if let Some(el) = node.args.get(1) {
check_callback(ctx, &el.expr);
}
}
}
}
}
}
fn is_map_member(expr: &Expr) -> bool {
match expr {
Expr::Member(member) => {
if let MemberProp::Ident(member_id) = member.prop {
if member_id.sym() == "map" {
return true;
}
}
false
}
Expr::OptChain(opt_member) => {
if let OptChainBase::Member(member) = opt_member.base {
if let MemberProp::Ident(member_id) = member.prop {
if member_id.sym() == "map" {
return true;
}
}
}
false
}
_ => false,
}
}
fn check_callback(ctx: &mut Context, expr: &Expr) {
match expr {
Expr::Arrow(arrow_fn) => match arrow_fn.body {
BlockStmtOrExpr::BlockStmt(stmt) => check_stmt(ctx, &Stmt::Block(stmt)),
BlockStmtOrExpr::Expr(expr) => check_expr(ctx, &expr),
},
Expr::Fn(fn_expr) => {
if let Some(body) = fn_expr.function.body {
check_stmt(ctx, &Stmt::Block(body))
}
}
_ => {}
}
}
fn check_stmt(ctx: &mut Context, stmt: &Stmt) {
match stmt {
Stmt::Return(return_stmt) => {
if let Some(arg) = return_stmt.arg {
check_expr(ctx, &arg);
}
}
Stmt::If(if_stmt) => {
check_stmt(ctx, &if_stmt.cons);
if let Some(alt) = if_stmt.alt {
check_stmt(ctx, &alt);
}
}
Stmt::Block(block_stmt) => {
for stmt in block_stmt.stmts {
check_stmt(ctx, stmt);
}
}
_ => {}
}
}
fn check_expr(ctx: &mut Context, expr: &Expr) {
match expr {
Expr::JSXElement(jsx_el) => {
if !has_key_jsx_attr(jsx_el.opening.attrs) {
ctx.add_diagnostic_with_hint(
jsx_el.opening.range(),
CODE,
DiagnosticKind::MissingKey.message(),
DiagnosticKind::MissingKey.hint(),
);
}
}
Expr::JSXFragment(jsx_frag) => {
ctx.add_diagnostic_with_hint(
jsx_frag.opening.range(),
CODE,
DiagnosticKind::MissingFragKey.message(),
DiagnosticKind::MissingFragKey.hint(),
);
}
Expr::Cond(cond_epxr) => {
check_expr(ctx, &cond_epxr.cons);
check_expr(ctx, &cond_epxr.alt);
}
Expr::Bin(bin_expr) => {
check_expr(ctx, &bin_expr.left);
check_expr(ctx, &bin_expr.right);
}
_ => {}
}
}
fn has_key_jsx_attr(attrs: &[JSXAttrOrSpread]) -> bool {
for attr in attrs {
if let JSXAttrOrSpread::JSXAttr(attr) = attr {
if let JSXAttrName::Ident(id) = attr.name {
if id.sym() == "key" {
return true;
}
}
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn jsx_key_valid() {
assert_lint_ok! {
JSXKey,
filename: "file:///foo.jsx",
"[1, 2, 3].map(x => {})",
"<div />",
r#"[<div key="1"/>, <div key="2" />]"#,
r#"[1, 2, 3].map(function(x) { return <div key={x} /> })"#,
r#"[1, 2, 3].map((x) => { return <div key={x} /> })"#,
r#"[1, 2, 3].map((x) => <div key={x} />)"#,
r#"[1, 2, 3].map((x) => foo && <div key={x} />)"#,
r#"[1, 2, 3].map((x) => foo ? <div key={x} /> : <div key={x} />)"#,
r#"[1, 2, 3]?.map((x) => <div key={x} />)"#,
r#"Array.from([1, 2, 3], function(x) { return <div key={x} /> })"#,
r#"Array.from([1, 2, 3], (x) => { return <div key={x} /> })"#,
r#"Array.from([1, 2, 3], (x) => <div key={x} />)"#,
r#"const Foo = () => {
const a = [1, 2, 3];
return (
<div>
{a.map(x => {
if (a) {
return <div key="a" />
}
return <div key="f" />
})}
</div>
);
}"#,
r#"const Foo = () => {
const a = [1, 2, 3];
return (
<div>
{a.map(x => {
if (a) return <div key="a" />
else return <div key="a" />
})}
</div>
);
}"#,
r#"const Foo = () => {
const a = [1, 2, 3];
return (
<div>
{a.map(x => <div key="a" />)}
</div>
);
}"#
};
}
#[test]
fn jsx_key_invalid() {
assert_lint_err! {
JSXKey,
filename: "file:///foo.jsx",
"[<div />]": [
{
col: 1,
message: DiagnosticKind::MissingKey.message(),
hint: DiagnosticKind::MissingKey.hint(),
}
],
r#"[<div key="foo" />, <div />]"#: [
{
col: 20,
message: DiagnosticKind::MissingKey.message(),
hint: DiagnosticKind::MissingKey.hint(),
}
],
r#"[1, 2, 3].map(function(x) { return <div /> });"#: [
{
col: 35,
message: DiagnosticKind::MissingKey.message(),
hint: DiagnosticKind::MissingKey.hint(),
}
],
r#"[1, 2, 3].map(x => <div />);"#: [
{
col: 19,
message: DiagnosticKind::MissingKey.message(),
hint: DiagnosticKind::MissingKey.hint(),
}
],
r#"[1, 2, 3].map(x => { return <div />; });"#: [
{
col: 28,
message: DiagnosticKind::MissingKey.message(),
hint: DiagnosticKind::MissingKey.hint(),
}
],
r#"[1, 2, 3].map(x => foo && <div />);"#: [
{
col: 26,
message: DiagnosticKind::MissingKey.message(),
hint: DiagnosticKind::MissingKey.hint(),
}
],
r#"[1, 2, 3].map(x => foo ? <div key="foo" /> : <div />);"#: [
{
col: 45,
message: DiagnosticKind::MissingKey.message(),
hint: DiagnosticKind::MissingKey.hint(),
}
],
r#"[1, 2, 3].map(x => foo ? <div /> : <div key="foo" />);"#: [
{
col: 25,
message: DiagnosticKind::MissingKey.message(),
hint: DiagnosticKind::MissingKey.hint(),
}
],
r#"[1, 2, 3]?.map(x => <div />);"#: [
{
col: 20,
message: DiagnosticKind::MissingKey.message(),
hint: DiagnosticKind::MissingKey.hint(),
}
],
r#"[<></>];"#: [
{
col: 1,
message: DiagnosticKind::MissingFragKey.message(),
hint: DiagnosticKind::MissingFragKey.hint(),
}
],
r#"const a = [<></>];"#: [
{
col: 11,
message: DiagnosticKind::MissingFragKey.message(),
hint: DiagnosticKind::MissingFragKey.hint(),
}
],
r#"Array.from([1, 2, 3], function(x) { return <div /> });"#: [
{
col: 43,
message: DiagnosticKind::MissingKey.message(),
hint: DiagnosticKind::MissingKey.hint(),
}
],
r#"Array.from([1, 2, 3], (x) => { return <div /> });"#: [
{
col: 38,
message: DiagnosticKind::MissingKey.message(),
hint: DiagnosticKind::MissingKey.hint(),
}
],
r#"Array.from([1, 2, 3], (x) => <div />);"#: [
{
col: 29,
message: DiagnosticKind::MissingKey.message(),
hint: DiagnosticKind::MissingKey.hint(),
}
],
r#"const Foo = () => {
const a = [1, 2, 3];
return (
<div>
{a.map(x => {
if (a) {
return <div />
}
return <div key="f" />
})}
</div>
);
}"#: [
{
line: 7,
col: 17,
message: DiagnosticKind::MissingKey.message(),
hint: DiagnosticKind::MissingKey.hint(),
}
],
r#"const Foo = () => {
const a = [1, 2, 3];
return (
<div>
{a.map(x => {
if (a) return <div />
return <div key="f" />
})}
</div>
);
}"#: [
{
line: 6,
col: 22,
message: DiagnosticKind::MissingKey.message(),
hint: DiagnosticKind::MissingKey.hint(),
}
],
r#"const Foo = () => {
const a = [1, 2, 3];
return (
<div>
{a.map(x => {
if (a) return <div key="f" />
else return <div />;
return <div key="f" />
})}
</div>
);
}"#: [
{
line: 7,
col: 20,
message: DiagnosticKind::MissingKey.message(),
hint: DiagnosticKind::MissingKey.hint(),
}
],
r#"const Foo = () => {
const a = [1, 2, 3];
return (
<div>
{a.map(x => {
return <div />
})}
</div>
);
}"#: [
{
line: 6,
col: 15,
message: DiagnosticKind::MissingKey.message(),
hint: DiagnosticKind::MissingKey.hint(),
}
],
r#"const Foo = () => {
const a = [1, 2, 3];
return (
<div>
{a.map(x => <div />)}
</div>
);
}"#: [
{
line: 5,
col: 18,
message: DiagnosticKind::MissingKey.message(),
hint: DiagnosticKind::MissingKey.hint(),
}
],
};
}
}