Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(linter): jsx-no-undef for eslint-plugin-react #1862

Merged
merged 4 commits into from
Dec 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ mod react {
pub mod jsx_key;
pub mod jsx_no_comment_text_nodes;
pub mod jsx_no_duplicate_props;
pub mod jsx_no_undef;
pub mod jsx_no_useless_fragment;
pub mod no_children_prop;
pub mod no_dangerously_set_inner_html;
Expand Down Expand Up @@ -453,6 +454,7 @@ oxc_macros::declare_all_lint_rules! {
react::jsx_no_comment_text_nodes,
react::jsx_no_duplicate_props,
react::jsx_no_useless_fragment,
react::jsx_no_undef,
react::react_in_jsx_scope,
react::no_children_prop,
react::no_dangerously_set_inner_html,
Expand Down
125 changes: 125 additions & 0 deletions crates/oxc_linter/src/rules/react/jsx_no_undef.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
use oxc_ast::{
ast::{
JSXElementName, JSXIdentifier, JSXMemberExpression, JSXMemberExpressionObject,
JSXOpeningElement,
},
AstKind,
};
use oxc_diagnostics::{
miette::{self, Diagnostic},
thiserror::Error,
};
use oxc_macros::declare_oxc_lint;
use oxc_span::{Atom, Span};

use crate::{context::LintContext, rule::Rule, AstNode};

#[derive(Debug, Error, Diagnostic)]
#[error("eslint-plugin-react(jsx-no-undef): Disallow undeclared variables in JSX")]
#[diagnostic(severity(warning), help("'{0}' is not defined."))]
struct JsxNoUndefDiagnostic(Atom, #[label] pub Span);

#[derive(Debug, Default, Clone)]
pub struct JsxNoUndef;

declare_oxc_lint!(
/// ### What it does
/// Disallow undeclared variables in JSX
///
/// ### Why is this bad?
/// It is most likely a potential ReferenceError caused by a misspelling of a variable or parameter name.
///
/// ### Example
/// ```jsx
/// const A = () => <App />
/// const C = <B />
/// ```
JsxNoUndef,
correctness
);

fn get_member_ident<'a>(expr: &'a JSXMemberExpression<'a>) -> &'a JSXIdentifier {
match expr.object {
JSXMemberExpressionObject::Identifier(ref ident) => ident,
JSXMemberExpressionObject::MemberExpression(ref next_expr) => get_member_ident(next_expr),
}
}
fn get_resolvable_ident<'a>(node: &'a JSXElementName<'a>) -> Option<&'a JSXIdentifier> {
match node {
JSXElementName::Identifier(ref ident)
if !(ident.name.as_str().starts_with(char::is_lowercase)) =>
{
Some(ident)
}
JSXElementName::Identifier(_) | JSXElementName::NamespacedName(_) => None,
JSXElementName::MemberExpression(expr) => Some(get_member_ident(expr)),
}
}

impl Rule for JsxNoUndef {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
if let AstKind::JSXOpeningElement(JSXOpeningElement { name: el_name, .. }) = &node.kind() {
if let Some(ident) = get_resolvable_ident(el_name) {
if ident.name.as_str() == "this" {
return;
}
let has_binding = ctx
.symbols()
.get_scope_id_from_name(&ident.name)
.map_or(false, |scope_id| ctx.scopes().has_binding(scope_id, &ident.name));

if !has_binding {
ctx.diagnostic(JsxNoUndefDiagnostic(ident.name.clone(), ident.span));
}
}
}
}
}

#[test]
fn test() {
use crate::tester::Tester;

let pass = vec![
("var React, App; React.render(<App />);", None),
("var React; React.render(<img />);", None),
("var React; React.render(<x-gif />);", None),
("var React, app; React.render(<app.Foo />);", None),
("var React, app; React.render(<app.foo.Bar />);", None),
("var React; React.render(<Apppp:Foo />);", None),
(
r"
var React;
class Hello extends React.Component {
render() {
return <this.props.tag />
}
}
",
None,
),
// TODO: Text should be declared in globals ("var React; React.render(<Text />);", None),
(
r#"
import Text from "cool-module";
const TextWrapper = function (props) {
return (
<Text />
);
};
"#,
None,
),
];

let fail = vec![
("var React; React.render(<App />);", None),
("var React; React.render(<Appp.Foo />);", None),
("var React; React.render(<appp.Foo />);", None),
("var React; React.render(<appp.foo.Bar />);", None),
("var React; React.render(<Foo />);", None),
("var React; Unknown; React.render(<Unknown />)", None),
];

Tester::new(JsxNoUndef::NAME, pass, fail).test_and_snapshot();
}
48 changes: 48 additions & 0 deletions crates/oxc_linter/src/snapshots/jsx_no_undef.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
source: crates/oxc_linter/src/tester.rs
assertion_line: 144
expression: jsx_no_undef
---
⚠ eslint-plugin-react(jsx-no-undef): Disallow undeclared variables in JSX
╭─[jsx_no_undef.tsx:1:1]
1 │ var React; React.render(<App />);
· ───
╰────
help: 'App' is not defined.

⚠ eslint-plugin-react(jsx-no-undef): Disallow undeclared variables in JSX
╭─[jsx_no_undef.tsx:1:1]
1 │ var React; React.render(<Appp.Foo />);
· ────
╰────
help: 'Appp' is not defined.

⚠ eslint-plugin-react(jsx-no-undef): Disallow undeclared variables in JSX
╭─[jsx_no_undef.tsx:1:1]
1 │ var React; React.render(<appp.Foo />);
· ────
╰────
help: 'appp' is not defined.

⚠ eslint-plugin-react(jsx-no-undef): Disallow undeclared variables in JSX
╭─[jsx_no_undef.tsx:1:1]
1 │ var React; React.render(<appp.foo.Bar />);
· ────
╰────
help: 'appp' is not defined.

⚠ eslint-plugin-react(jsx-no-undef): Disallow undeclared variables in JSX
╭─[jsx_no_undef.tsx:1:1]
1 │ var React; React.render(<Foo />);
· ───
╰────
help: 'Foo' is not defined.

⚠ eslint-plugin-react(jsx-no-undef): Disallow undeclared variables in JSX
╭─[jsx_no_undef.tsx:1:1]
1 │ var React; Unknown; React.render(<Unknown />)
· ───────
╰────
help: 'Unknown' is not defined.