Skip to content
Open
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
1 change: 1 addition & 0 deletions schemas/rules.v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"jsx-key",
"jsx-no-children-prop",
"jsx-no-comment-text-nodes",
"jsx-no-conflicting-pragmas",
"jsx-no-danger-with-children",
"jsx-no-duplicate-props",
"jsx-no-unescaped-entities",
Expand Down
2 changes: 2 additions & 0 deletions src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub mod jsx_curly_braces;
pub mod jsx_key;
pub mod jsx_no_children_prop;
pub mod jsx_no_comment_text_nodes;
pub mod jsx_no_conflicting_pragmas;
pub mod jsx_no_duplicate_props;
pub mod jsx_no_unescaped_entities;
pub mod jsx_no_useless_fragment;
Expand Down Expand Up @@ -275,6 +276,7 @@ fn get_all_rules_raw() -> Vec<Box<dyn LintRule>> {
Box::new(jsx_key::JSXKey),
Box::new(jsx_no_children_prop::JSXNoChildrenProp),
Box::new(jsx_no_comment_text_nodes::JSXNoCommentTextNodes),
Box::new(jsx_no_conflicting_pragmas::JSXNoConflictingPragmas),
Box::new(jsx_no_duplicate_props::JSXNoDuplicateProps),
Box::new(jsx_no_unescaped_entities::JSXNoUnescapedEntities),
Box::new(jsx_no_useless_fragment::JSXNoUselessFragment),
Expand Down
156 changes: 156 additions & 0 deletions src/rules/jsx_no_conflicting_pragmas.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

use super::{Context, LintRule};
use crate::tags::{self, Tags};
use crate::Program;
use deno_ast::SourceRange;
use deno_ast::SourceRanged;
use deno_ast::SourceRangedForSpanned;
use once_cell::sync::Lazy;
use regex::Regex;

#[derive(Debug)]
pub struct JSXNoConflictingPragmas;

const CODE: &str = "jsx-no-conflicting-pragmas";
const MESSAGE: &str = "Conflicting JSX pragmas";
const HINT: &str = "The classic runtime pragmas `@jsx` and `@jsxFragment` are ignored when `@jsxImportSource` (the automatic runtime) is set. Use one runtime or the other, not both.";

// `@jsxImportSource` selects the automatic JSX runtime. The trailing word
// boundary makes sure the types-only `@jsxImportSourceTypes` directive,
// which doesn't switch the runtime on its own, is not matched here.
static IMPORT_SOURCE_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"@jsxImportSource\b").unwrap());

// `@jsx`, `@jsxFrag` and `@jsxFragment` are classic runtime pragmas. The
// trailing word boundary makes sure we don't match `@jsxImportSource` or
// `@jsxRuntime`.
static CLASSIC_PRAGMA_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"@jsx(Frag(ment)?)?\b").unwrap());

impl LintRule for JSXNoConflictingPragmas {
fn tags(&self) -> Tags {
&[tags::REACT, tags::JSX]
}

fn code(&self) -> &'static str {
CODE
}

fn lint_program_with_ast_view(
&self,
context: &mut Context,
program: Program,
) {
// JSX pragmas only take effect in the file's leading comments, so only
// consider comments that appear before the first statement.
let first_item_start = match program {
Program::Module(module) => module.body.first().map(|n| n.start()),
Program::Script(script) => script.body.first().map(|n| n.start()),
};

let mut has_import_source = false;
let mut classic_pragma_ranges: Vec<SourceRange> = Vec::new();

for comment in context.all_comments() {
if let Some(first_item_start) = first_item_start {
if comment.start() >= first_item_start {
continue;
}
}

if IMPORT_SOURCE_RE.is_match(&comment.text) {
has_import_source = true;
}

// A comment can hold the `@jsxImportSource` pragma and still not be a
// classic pragma, so check both independently.
if CLASSIC_PRAGMA_RE.is_match(&comment.text) {
classic_pragma_ranges.push(comment.range());
}
}

if has_import_source {
for range in classic_pragma_ranges {
context.add_diagnostic_with_hint(range, CODE, MESSAGE, HINT);
}
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn jsx_no_conflicting_pragmas_valid() {
assert_lint_ok! {
JSXNoConflictingPragmas,
filename: "file:///foo.jsx",
// Only the automatic runtime.
r#"/** @jsxImportSource https://esm.sh/preact */
const a = <div />;"#,
// Only the classic runtime.
r#"/** @jsx h */
/** @jsxFragment Fragment */
const a = <div />;"#,
// `@jsxImportSource` paired with `@jsxImportSourceTypes` is fine.
r#"/** @jsxImportSource https://esm.sh/preact */
/** @jsxImportSourceTypes https://esm.sh/preact */
const a = <div />;"#,
// `@jsxImportSourceTypes` alone doesn't switch the runtime, so it
// doesn't conflict with the classic pragmas.
r#"/** @jsxImportSourceTypes https://esm.sh/preact */
/** @jsx h */
const a = <div />;"#,
// No pragmas at all.
r#"const a = <div />;"#,
};
}

#[test]
fn jsx_no_conflicting_pragmas_invalid() {
assert_lint_err! {
JSXNoConflictingPragmas,
filename: "file:///foo.jsx",
r#"/** @jsxImportSource https://esm.sh/preact */
/** @jsx h */
const a = <div />;"#: [
{
line: 2,
col: 0,
message: MESSAGE,
hint: HINT,
}
],
r#"/** @jsxImportSource https://esm.sh/preact */
/** @jsx h */
/** @jsxFragment Fragment */
const a = <div />;"#: [
{
line: 2,
col: 0,
message: MESSAGE,
hint: HINT,
},
{
line: 3,
col: 0,
message: MESSAGE,
hint: HINT,
}
],
// Order doesn't matter.
r#"/** @jsx h */
/** @jsxImportSource https://esm.sh/preact */
const a = <div />;"#: [
{
line: 1,
col: 0,
message: MESSAGE,
hint: HINT,
}
],
};
}
}