Skip to content

Commit 7d5a279

Browse files
committed
fix(fe): don't report React-specific JSX errors in non-React code
Preact uses JSX but has different rules for attributes. Disable our React-specific rules if a React import is not detected.
1 parent 9cdb42c commit 7d5a279

File tree

9 files changed

+179
-20
lines changed

9 files changed

+179
-20
lines changed

docs/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ Semantic Versioning.
2020

2121
* quick-lint-js's tracing no longer crashes with an assertion failure when
2222
setting its thread name on FreeBSD.
23+
* React-specific JSX diagnostics, such as [E0193][] ("misspelled React
24+
attribute; write 'className' instead"), are now only reported when 'react' is
25+
imported. This fixes false warnings in Preact code. ([#1152][])
2326

2427
## 3.0.0 (2024-01-01)
2528

@@ -1376,6 +1379,8 @@ Beta release.
13761379
[toastin0]: https://github.com/toastin0
13771380
[wagner riffel]: https://github.com/wgrr
13781381

1382+
[#1152]: https://github.com/quick-lint/quick-lint-js/issues/1152
1383+
13791384
[E0001]: https://quick-lint-js.com/errors/E0001/
13801385
[E0003]: https://quick-lint-js.com/errors/E0003/
13811386
[E0013]: https://quick-lint-js.com/errors/E0013/

docs/errors/E0191.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ In HTML, attributes are case-insensitive; `onclick` is the same as `onClick` and
55
attribute (starting with `on`) to be all lower-case:
66

77
```javascript-jsx
8+
import React from "react";
9+
810
function TodoEntry({addTodo, changePendingTodo}) {
911
return <form onsubmit={addTodo}>
1012
<input onchange={changePendingTodo} />
@@ -17,6 +19,8 @@ To fix this error, fix the capitalization by writing the attribute in
1719
lowerCamelCase:
1820

1921
```javascript-jsx
22+
import React from "react";
23+
2024
function TodoEntry({addTodo, changePendingTodo}) {
2125
return <form onSubmit={addTodo}>
2226
<input onChange={changePendingTodo} />

docs/errors/E0192.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ In HTML, attributes are case-insensitive; `colspan` is the same as `colSpan` and
55
attribute for a built-in element to have the wrong capitalization:
66

77
```javascript-jsx
8+
import React from "react";
9+
810
function Header({columns}) {
911
return <tr>
1012
<th colspan="2">Name</th>
@@ -17,6 +19,8 @@ function Header({columns}) {
1719
To fix this error, fix the capitalization of the attribute:
1820

1921
```javascript-jsx
22+
import React from "react";
23+
2024
function Header({columns}) {
2125
return <tr>
2226
<th colSpan="2">Name</th>

docs/errors/E0193.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ React has a different name for some attributes than HTML. It is a mistake to
44
write the HTML attribute instead of the React attribute:
55

66
```javascript-jsx
7+
import React from "react";
8+
79
function Title({page}) {
810
return <h1 class="title">
911
<a href={page.url} class="page-link">
@@ -16,6 +18,8 @@ function Title({page}) {
1618
To fix this error, write the name of the attribute understood by React:
1719

1820
```javascript-jsx
21+
import React from "react";
22+
1923
function Title({page}) {
2024
return <h1 className="title">
2125
<a href={page.url} className="page-link">

src/quick-lint-js/fe/parse-expression.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3851,7 +3851,7 @@ Expression* Parser::parse_jsx_element_or_fragment(Parse_Visitor_Base& v,
38513851
this->lexer_.skip_in_jsx();
38523852
}
38533853
if (is_intrinsic && !has_namespace && !tag_namespace) {
3854-
this->check_jsx_attribute(attribute);
3854+
this->jsx_intrinsic_attributes_.emplace_back(attribute);
38553855
}
38563856
if (this->peek().type == Token_Type::equal) {
38573857
this->lexer_.skip_in_jsx();

src/quick-lint-js/fe/parse-statement.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ void Parser::parse_and_visit_module(Parse_Visitor_Base &v) {
6868
}
6969
}
7070
}
71+
this->check_all_jsx_attributes();
7172
v.visit_end_of_module();
7273
}
7374

@@ -4695,6 +4696,7 @@ void Parser::parse_and_visit_import(
46954696
// import "foo";
46964697
case Token_Type::string:
46974698
// Do not set is_current_typescript_namespace_non_empty_.
4699+
this->visited_module_import(this->peek());
46984700
this->skip();
46994701
this->consume_semicolon_after_statement();
47004702
return;
@@ -4853,6 +4855,7 @@ void Parser::parse_and_visit_import(
48534855

48544856
this->skip();
48554857
QLJS_PARSER_UNIMPLEMENTED_IF_NOT_TOKEN(Token_Type::string);
4858+
this->visited_module_import(this->peek());
48564859
this->skip();
48574860
QLJS_PARSER_UNIMPLEMENTED_IF_NOT_TOKEN(Token_Type::right_paren);
48584861
this->skip();
@@ -4918,6 +4921,7 @@ void Parser::parse_and_visit_import(
49184921
.declare_keyword = *declare_context.declare_namespace_declare_keyword,
49194922
});
49204923
}
4924+
this->visited_module_import(this->peek());
49214925
this->skip();
49224926
break;
49234927

@@ -5313,6 +5317,18 @@ void Parser::parse_and_visit_named_exports(
53135317
this->skip();
53145318
}
53155319

5320+
void Parser::visited_module_import(const Token &module_name) {
5321+
QLJS_ASSERT(module_name.type == Token_Type::string);
5322+
// TODO(#1159): Write a proper routine to decode string literals.
5323+
String8_View module_name_unescaped =
5324+
make_string_view(module_name.begin + 1, module_name.end - 1);
5325+
if (module_name_unescaped == u8"react"_sv ||
5326+
module_name_unescaped == u8"react-dom"_sv ||
5327+
starts_with(module_name_unescaped, u8"react-dom/"_sv)) {
5328+
this->imported_react_ = true;
5329+
}
5330+
}
5331+
53165332
void Parser::parse_and_visit_variable_declaration_statement(
53175333
Parse_Visitor_Base &v,
53185334
bool is_top_level_typescript_definition_without_declare_or_export) {

src/quick-lint-js/fe/parse.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,27 @@ Expression* Parser::build_expression(Binary_Expression_Builder& builder) {
158158
}
159159
}
160160

161+
void Parser::check_all_jsx_attributes() {
162+
switch (this->options_.jsx_mode) {
163+
case Parser_JSX_Mode::none:
164+
break;
165+
166+
case Parser_JSX_Mode::auto_detect:
167+
if (this->imported_react_) {
168+
goto react;
169+
}
170+
break;
171+
172+
react:
173+
case Parser_JSX_Mode::react:
174+
this->jsx_intrinsic_attributes_.for_each(
175+
[&](const Identifier& attribute_name) -> void {
176+
this->check_jsx_attribute(attribute_name);
177+
});
178+
break;
179+
}
180+
}
181+
161182
QLJS_WARNING_PUSH
162183
QLJS_WARNING_IGNORE_GCC("-Wnull-dereference")
163184
void Parser::check_jsx_attribute(const Identifier& attribute_name) {

src/quick-lint-js/fe/parse.h

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <optional>
88
#include <quick-lint-js/assert.h>
99
#include <quick-lint-js/container/hash-map.h>
10+
#include <quick-lint-js/container/linked-vector.h>
1011
#include <quick-lint-js/container/padded-string.h>
1112
#include <quick-lint-js/diag/diag-reporter.h>
1213
#include <quick-lint-js/diag/diagnostic-types.h>
@@ -55,11 +56,30 @@ enum class Parser_Top_Level_Await_Mode {
5556
await_operator,
5657
};
5758

59+
// How to interpret props for builtins such as <button onclick> and <span
60+
// class>.
61+
enum class Parser_JSX_Mode {
62+
// Detect the JSX mode based on heuristics.
63+
auto_detect = 0,
64+
65+
// Enforce no rules.
66+
none,
67+
68+
// Use React's rules:
69+
// * camelCase for event handler attributes starting with 'on'
70+
// * certain camelCase attributes such as 'colSpan'
71+
// * 'class' is named 'className'
72+
react,
73+
};
74+
5875
// TODO(#465): Accept parser options from quick-lint-js.config or CLI options.
5976
struct Parser_Options {
6077
Parser_Top_Level_Await_Mode top_level_await_mode =
6178
Parser_Top_Level_Await_Mode::auto_detect;
6279

80+
// jsx_mode does not affect whether JSX is parsed or not. See this->jsx.
81+
Parser_JSX_Mode jsx_mode = Parser_JSX_Mode::auto_detect;
82+
6383
// If true, parse JSX language extensions: https://facebook.github.io/jsx/
6484
bool jsx = false;
6585

@@ -608,6 +628,9 @@ class Parser {
608628
void parse_and_visit_named_exports_for_typescript_type_only_import(
609629
Parse_Visitor_Base &v, Source_Code_Span type_keyword);
610630

631+
// Precondition: module_name.type == Token_Type::string;
632+
void visited_module_import(const Token &module_name);
633+
611634
// If set, refers to the first `export default` statement in this module. A
612635
// module cannot contain more than one `export default`.
613636
std::optional<Source_Code_Span>
@@ -887,6 +910,7 @@ class Parser {
887910
Expression *parse_jsx_element_or_fragment(Parse_Visitor_Base &,
888911
Identifier *tag,
889912
const Char8 *less_begin);
913+
void check_all_jsx_attributes();
890914
void check_jsx_attribute(const Identifier &attribute_name);
891915
Expression *parse_typescript_generic_arrow_expression(Parse_Visitor_Base &,
892916
Precedence);
@@ -1092,6 +1116,15 @@ class Parser {
10921116
// variables) so that memory can be released in case we call setjmp.
10931117
Buffering_Visitor_Stack buffering_visitor_stack_;
10941118

1119+
// All parsed JSX attributes for intrinsic elements.
1120+
//
1121+
// Depending on Parser_JSX_Mode, diagnostics may or may not be reported for
1122+
// these attributes at the end of a module.
1123+
Linked_Vector<Identifier> jsx_intrinsic_attributes_{new_delete_resource()};
1124+
1125+
// Heuristic. True if React.js was imported.
1126+
bool imported_react_ = false;
1127+
10951128
bool in_top_level_ = true;
10961129
bool in_async_function_ = false;
10971130
bool in_generator_function_ = false;

0 commit comments

Comments
 (0)