Skip to content

Commit 81cda61

Browse files
committed
feat(js_analyze): implement noSyncScripts
1 parent f4433b3 commit 81cda61

File tree

15 files changed

+359
-65
lines changed

15 files changed

+359
-65
lines changed

.changeset/icy-bags-sleep.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added the nursery rule [`noSyncScripts`](https://biomejs.dev/linter/rules/no-sync-scripts/). Prevent the usage of synchronous scripts.
6+
7+
**Invalid:**
8+
9+
```jsx
10+
<script src="https://third-party-script.js" />
11+
```
12+
13+
**Valid:**
14+
15+
```jsx
16+
<script src="https://third-party-script.js" async />
17+
<script src="https://third-party-script.js" defer />
18+
```

crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_configuration/src/analyzer/linter/rules.rs

Lines changed: 85 additions & 64 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_configuration/src/generated/domain_selector.rs

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_diagnostics_categories/src/categories.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ define_categories! {
181181
"lint/nursery/noParametersOnlyUsedInRecursion": "https://biomejs.dev/linter/rules/no-parameters-only-used-in-recursion",
182182
"lint/nursery/noReactForwardRef": "https://biomejs.dev/linter/rules/no-react-forward-ref",
183183
"lint/nursery/noShadow": "https://biomejs.dev/linter/rules/no-shadow",
184+
"lint/nursery/noSyncScripts": "https://biomejs.dev/linter/rules/no-sync-scripts",
184185
"lint/nursery/noUnknownAttribute": "https://biomejs.dev/linter/rules/no-unknown-attribute",
185186
"lint/nursery/noUnnecessaryConditions": "https://biomejs.dev/linter/rules/no-unnecessary-conditions",
186187
"lint/nursery/noUnresolvedImports": "https://biomejs.dev/linter/rules/no-unresolved-imports",

crates/biome_js_analyze/src/lint/nursery.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub mod no_next_async_client_component;
1616
pub mod no_parameters_only_used_in_recursion;
1717
pub mod no_react_forward_ref;
1818
pub mod no_shadow;
19+
pub mod no_sync_scripts;
1920
pub mod no_unknown_attribute;
2021
pub mod no_unnecessary_conditions;
2122
pub mod no_unresolved_imports;
@@ -38,4 +39,4 @@ pub mod use_sorted_classes;
3839
pub mod use_spread;
3940
pub mod use_vue_define_macros_order;
4041
pub mod use_vue_multi_word_component_names;
41-
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_empty_source :: NoEmptySource , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_misused_promises :: NoMisusedPromises , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_shadow :: NoShadow , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
42+
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_empty_source :: NoEmptySource , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_misused_promises :: NoMisusedPromises , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
use biome_analyze::{
2+
Ast, Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule,
3+
};
4+
use biome_console::markup;
5+
use biome_js_syntax::{
6+
AnyJsxElementName, JsxAttributeList, JsxOpeningElement, JsxSelfClosingElement,
7+
};
8+
use biome_rowan::{AstNode, declare_node_union};
9+
use biome_rule_options::no_sync_scripts::NoSyncScriptsOptions;
10+
11+
declare_lint_rule! {
12+
/// Prevent the usage of synchronous scripts.
13+
///
14+
/// A synchronous script which impact your webpage performance, read more on how to [Efficiently load third-party JavaScript](https://web.dev/articles/efficiently-load-third-party-javascript).
15+
///
16+
/// ## Examples
17+
///
18+
/// ### Invalid
19+
///
20+
/// ```jsx,expect_diagnostic
21+
/// const Invalid = () => <script src="https://third-party-script.js" />;
22+
/// ```
23+
///
24+
/// ### Valid
25+
///
26+
/// ```jsx
27+
/// const Valid = () => {
28+
/// return (
29+
/// <>
30+
/// <script src="https://third-party-script.js" async />
31+
/// <script src="https://third-party-script.js" defer />
32+
/// </>
33+
/// );
34+
/// }
35+
/// ```
36+
///
37+
/// #### Next.js
38+
///
39+
/// ```jsx
40+
/// import Script from 'next/script'
41+
///
42+
/// const Valid = () => <Script src="https://third-party-script.js" />;
43+
/// ```
44+
///
45+
pub NoSyncScripts {
46+
version: "next",
47+
name: "noSyncScripts",
48+
language: "js",
49+
recommended: false,
50+
sources: &[RuleSource::EslintNext("no-sync-scripts").same()],
51+
domains: &[RuleDomain::React, RuleDomain::Next],
52+
}
53+
}
54+
55+
declare_node_union! {
56+
pub NoSyncScriptsQuery =
57+
JsxOpeningElement
58+
| JsxSelfClosingElement
59+
}
60+
61+
fn validate_name(node: &AnyJsxElementName) -> Option<()> {
62+
let name = node.as_jsx_name()?;
63+
let value_token = name.value_token().ok()?;
64+
65+
if value_token.text_trimmed() == "script" {
66+
return Some(());
67+
}
68+
69+
None
70+
}
71+
72+
fn validate_attributes(list: &JsxAttributeList) -> Option<()> {
73+
list.find_by_name("src")?;
74+
75+
if list.find_by_name("async").is_some() || list.find_by_name("defer").is_some() {
76+
return None;
77+
}
78+
79+
Some(())
80+
}
81+
82+
impl Rule for NoSyncScripts {
83+
type Query = Ast<NoSyncScriptsQuery>;
84+
type State = ();
85+
type Signals = Option<Self::State>;
86+
type Options = NoSyncScriptsOptions;
87+
88+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
89+
let binding = ctx.query();
90+
91+
match binding {
92+
NoSyncScriptsQuery::JsxOpeningElement(node) => {
93+
let name = node.name().ok()?;
94+
validate_name(&name)?;
95+
96+
let attributes = node.attributes();
97+
validate_attributes(&attributes)
98+
}
99+
NoSyncScriptsQuery::JsxSelfClosingElement(node) => {
100+
let name = node.name().ok()?;
101+
validate_name(&name)?;
102+
103+
let attributes = node.attributes();
104+
validate_attributes(&attributes)
105+
}
106+
}
107+
}
108+
109+
fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
110+
let node = ctx.query();
111+
Some(
112+
RuleDiagnostic::new(
113+
rule_category!(),
114+
node.range(),
115+
markup! {
116+
"Unexpected synchronous script."
117+
},
118+
)
119+
.note(markup! {
120+
"Synchronous scripts can impact your webpage performance. Add the \"async\" or \"defer\" attribute, or replace with NextJS' Script component."
121+
}),
122+
)
123+
}
124+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const Invalid = () => {
2+
return <script src="" />
3+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: invalid.jsx
4+
---
5+
# Input
6+
```jsx
7+
const Invalid = () => {
8+
return <script src="" />
9+
}
10+
11+
```
12+
13+
# Diagnostics
14+
```
15+
invalid.jsx:2:9 lint/nursery/noSyncScripts ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
16+
17+
i Unexpected synchronous script.
18+
19+
1 │ const Invalid = () => {
20+
> 2return <script src="" />
21+
^^^^^^^^^^^^^^^^^
22+
3}
23+
4 │
24+
25+
i Synchronous scripts can impact your webpage performance. Add the "async" or "defer" attribute, or replace with NextJS' Script component.
26+
27+
28+
```
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/* should not generate diagnostics */
2+
const Valid1 = () => {
3+
return <noscript />
4+
}
5+
6+
const Valid2 = () => {
7+
return <script />
8+
}
9+
10+
const Valid3 = () => {
11+
return <script src="" async />
12+
}
13+
14+
const Valid4 = () => {
15+
return <script src="" defer />
16+
}

0 commit comments

Comments
 (0)