Thanks to visit codestin.com
Credit goes to github.com

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
6 changes: 6 additions & 0 deletions .changeset/eight-eels-pull.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@biomejs/backend-jsonrpc": patch
"@biomejs/biome": patch
---

Fixed [#6438](https://github.com/biomejs/biome/issues/6438) and [#3682](https://github.com/biomejs/biome/issues/3682): Biome now respects jsxFactory and jsxFragmentFactory settings from tsconfig.json when using the classic JSX runtime, preventing false positive [noUnusedImports](https://biomejs.dev/linter/rules/no-unused-imports/) errors for custom JSX libraries like Preact.
16 changes: 16 additions & 0 deletions crates/biome_analyze/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ pub struct RuleContext<'a, R: Rule> {
preferred_jsx_quote: PreferredQuote,
preferred_indentation: PreferredIndentation,
jsx_runtime: Option<JsxRuntime>,
jsx_factory: Option<&'a str>,
jsx_fragment_factory: Option<&'a str>,
css_modules: bool,
}

Expand All @@ -39,6 +41,8 @@ where
preferred_jsx_quote: PreferredQuote,
preferred_indentation: PreferredIndentation,
jsx_runtime: Option<JsxRuntime>,
jsx_factory: Option<&'a str>,
jsx_fragment_factory: Option<&'a str>,
css_modules: bool,
) -> Result<Self, Error> {
let rule_key = RuleKey::rule::<R>();
Expand All @@ -54,6 +58,8 @@ where
preferred_jsx_quote,
preferred_indentation,
jsx_runtime,
jsx_factory,
jsx_fragment_factory,
css_modules,
})
}
Expand Down Expand Up @@ -155,6 +161,16 @@ where
self.jsx_runtime.expect("jsx_runtime should be provided")
}

/// Returns the JSX factory identifier (e.g., "h" or "React")
pub fn jsx_factory(&self) -> Option<&str> {
self.jsx_factory
}

/// Returns the JSX fragment factory identifier (e.g., "Fragment")
pub fn jsx_fragment_factory(&self) -> Option<&str> {
self.jsx_fragment_factory
}

/// Checks whether the provided text belongs to globals
pub fn is_global(&self, text: &str) -> bool {
self.globals.iter().any(|global| global.as_ref() == text)
Expand Down
32 changes: 32 additions & 0 deletions crates/biome_analyze/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ pub struct AnalyzerConfiguration {
/// Indicates the type of runtime or transformation used for interpreting JSX.
jsx_runtime: Option<JsxRuntime>,

/// The JSX factory function identifier (e.g., "h" from "h" or "React" from "React.createElement")
jsx_factory: Option<Box<str>>,

/// The JSX fragment factory function identifier (e.g., "Fragment")
jsx_fragment_factory: Option<Box<str>>,

/// Whether the CSS files contain CSS Modules
css_modules: bool,
}
Expand All @@ -95,6 +101,16 @@ impl AnalyzerConfiguration {
self
}

pub fn with_jsx_factory(mut self, jsx_factory: Option<Box<str>>) -> Self {
self.jsx_factory = jsx_factory;
self
}

pub fn with_jsx_fragment_factory(mut self, jsx_fragment_factory: Option<Box<str>>) -> Self {
self.jsx_fragment_factory = jsx_fragment_factory;
self
}

pub fn with_preferred_quote(mut self, preferred_quote: PreferredQuote) -> Self {
self.preferred_quote = preferred_quote;
self
Expand Down Expand Up @@ -160,6 +176,22 @@ impl AnalyzerOptions {
self.configuration.jsx_runtime
}

pub fn jsx_factory(&self) -> Option<&str> {
self.configuration.jsx_factory.as_deref()
}

pub fn set_jsx_factory(&mut self, jsx_factory: Option<Box<str>>) {
self.configuration.jsx_factory = jsx_factory;
}

pub fn set_jsx_fragment_factory(&mut self, jsx_fragment_factory: Option<Box<str>>) {
self.configuration.jsx_fragment_factory = jsx_fragment_factory;
}

pub fn jsx_fragment_factory(&self) -> Option<&str> {
self.configuration.jsx_fragment_factory.as_deref()
}

pub fn rule_options<R>(&self) -> Option<R::Options>
where
R: Rule<Options: Clone> + 'static,
Expand Down
4 changes: 4 additions & 0 deletions crates/biome_analyze/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,8 @@ impl<L: Language + Default> RegistryRule<L> {
let preferred_jsx_quote = params.options.preferred_jsx_quote();
let preferred_indentation = params.options.preferred_indentation();
let jsx_runtime = params.options.jsx_runtime();
let jsx_factory = params.options.jsx_factory();
let jsx_fragment_factory = params.options.jsx_fragment_factory();
let css_modules = params.options.css_modules();
let options = params.options.rule_options::<R>().unwrap_or_default();
let ctx = RuleContext::new(
Expand All @@ -419,6 +421,8 @@ impl<L: Language + Default> RegistryRule<L> {
preferred_jsx_quote,
preferred_indentation,
jsx_runtime,
jsx_factory,
jsx_fragment_factory,
css_modules,
)?;

Expand Down
6 changes: 6 additions & 0 deletions crates/biome_analyze/src/signals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,8 @@ where
preferred_jsx_quote,
preferred_indentation,
self.options.jsx_runtime(),
self.options.jsx_factory(),
self.options.jsx_fragment_factory(),
self.options.css_modules(),
)
.ok()?;
Expand Down Expand Up @@ -407,6 +409,8 @@ where
self.options.preferred_jsx_quote(),
self.options.preferred_indentation(),
self.options.jsx_runtime(),
self.options.jsx_factory(),
self.options.jsx_fragment_factory(),
self.options.css_modules(),
)
.ok();
Expand Down Expand Up @@ -472,6 +476,8 @@ where
self.options.preferred_jsx_quote(),
self.options.preferred_indentation(),
self.options.jsx_runtime(),
self.options.jsx_factory(),
self.options.jsx_fragment_factory(),
self.options.css_modules(),
)
.ok();
Expand Down
22 changes: 17 additions & 5 deletions crates/biome_js_analyze/src/lint/correctness/no_unused_imports.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::services::semantic::{SemanticModelBuilderVisitor, SemanticServices};
use crate::{
JsRuleAction,
react::{ReactLibrary, is_global_react_import},
react::{ReactLibrary, is_global_react_import, is_jsx_factory_import},
};
use biome_rule_options::no_unused_imports::NoUnusedImportsOptions;

Expand Down Expand Up @@ -623,10 +623,22 @@ fn is_unused(ctx: &RuleContext<NoUnusedImports>, local_name: &AnyJsBinding) -> b
let AnyJsBinding::JsIdentifierBinding(binding) = &local_name else {
return false;
};
if ctx.jsx_runtime() == JsxRuntime::ReactClassic
&& is_global_react_import(binding, ReactLibrary::React)
{
return false;
if ctx.jsx_runtime() == JsxRuntime::ReactClassic {
// Check for standard React import
if is_global_react_import(binding, ReactLibrary::React) {
return false;
}
// Check for custom JSX factory imports
if let Some(jsx_factory) = ctx.jsx_factory() {
if is_jsx_factory_import(binding, jsx_factory) {
return false;
}
}
if let Some(jsx_fragment_factory) = ctx.jsx_fragment_factory() {
if is_jsx_factory_import(binding, jsx_fragment_factory) {
return false;
}
}
}

let jsdoc_types = ctx.jsdoc_types();
Expand Down
49 changes: 36 additions & 13 deletions crates/biome_js_analyze/src/lint/style/use_import_type.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
JsRuleAction,
react::{ReactLibrary, is_global_react_import},
react::{ReactLibrary, is_global_react_import, is_jsx_factory_import},
services::semantic::Semantic,
};
use biome_analyze::{
Expand Down Expand Up @@ -192,9 +192,7 @@ impl Rule for UseImportType {
AnyJsImportClause::JsImportCombinedClause(clause) => {
let default_binding = clause.default_specifier().ok()?.local_name().ok()?;
let default_binding = default_binding.as_js_identifier_binding()?;
let is_default_used_as_type = if ctx.jsx_runtime() == JsxRuntime::ReactClassic
&& is_global_react_import(default_binding, ReactLibrary::React)
{
let is_default_used_as_type = if is_jsx_factory_binding(ctx, default_binding) {
false
} else {
is_only_used_as_type(model, default_binding)
Expand Down Expand Up @@ -250,9 +248,7 @@ impl Rule for UseImportType {
AnyJsCombinedSpecifier::JsNamespaceImportSpecifier(namespace_specifier) => {
let namespace_binding = namespace_specifier.local_name().ok()?;
let namespace_binding = namespace_binding.as_js_identifier_binding()?;
if ctx.jsx_runtime() == JsxRuntime::ReactClassic
&& is_global_react_import(namespace_binding, ReactLibrary::React)
{
if is_jsx_factory_binding(ctx, namespace_binding) {
return None;
}

Expand All @@ -278,9 +274,7 @@ impl Rule for UseImportType {
}
let default_binding = clause.default_specifier().ok()?.local_name().ok()?;
let default_binding = default_binding.as_js_identifier_binding()?;
if ctx.jsx_runtime() == JsxRuntime::ReactClassic
&& is_global_react_import(default_binding, ReactLibrary::React)
{
if is_jsx_factory_binding(ctx, default_binding) {
return None;
}

Expand Down Expand Up @@ -346,9 +340,7 @@ impl Rule for UseImportType {
}
let namespace_binding = clause.namespace_specifier().ok()?.local_name().ok()?;
let namespace_binding = namespace_binding.as_js_identifier_binding()?;
if ctx.jsx_runtime() == JsxRuntime::ReactClassic
&& is_global_react_import(namespace_binding, ReactLibrary::React)
{
if is_jsx_factory_binding(ctx, namespace_binding) {
return None;
}

Expand Down Expand Up @@ -1104,3 +1096,34 @@ fn split_named_import_specifiers(
};
Some((named_type, named_value))
}

/// Helper function to check if a binding is a JSX factory or fragment factory
fn is_jsx_factory_binding(
ctx: &RuleContext<UseImportType>,
binding: &biome_js_syntax::JsIdentifierBinding,
) -> bool {
if ctx.jsx_runtime() != JsxRuntime::ReactClassic {
return false;
}

// Check for standard React import
if is_global_react_import(binding, ReactLibrary::React) {
return true;
}

// Check for custom JSX factory
if let Some(jsx_factory) = ctx.jsx_factory() {
if is_jsx_factory_import(binding, jsx_factory) {
return true;
}
}

// Check for custom JSX fragment factory
if let Some(jsx_fragment_factory) = ctx.jsx_fragment_factory() {
if is_jsx_factory_import(binding, jsx_fragment_factory) {
return true;
}
}

false
}
9 changes: 9 additions & 0 deletions crates/biome_js_analyze/src/react.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,3 +334,12 @@ pub(crate) fn is_global_react_import(binding: &JsIdentifierBinding, lib: ReactLi
.and_then(|import| import.source_text().ok())
.is_some_and(|source| lib.import_names().contains(&source.text()))
}

/// Checks if `binding` matches a custom JSX factory identifier.
/// This is used when `jsxRuntime` is `ReactClassic` and custom JSX factories are configured
/// via tsconfig.json's `jsxFactory` or `jsxFragmentFactory` options.
pub(crate) fn is_jsx_factory_import(binding: &JsIdentifierBinding, factory_name: &str) -> bool {
binding
.name_token()
.is_ok_and(|name| name.text_trimmed() == factory_name)
}
59 changes: 59 additions & 0 deletions crates/biome_js_analyze/tests/quick_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,62 @@ export function f(options: PostcssOptions) {

// assert_eq!(error_ranges.as_slice(), &[]);
}

#[test]
fn test_jsx_factory_from_tsconfig() {
const FILENAME: &str = "test.jsx";
const SOURCE: &str = r#"import { h, Fragment } from 'preact';
function App() {
return (
<>
<div>Hello World</div>
</>
);
}
"#;

let parsed = parse(SOURCE, JsFileSource::jsx(), JsParserOptions::default());

let file_path = Utf8PathBuf::from(FILENAME);

let mut error_ranges: Vec<TextRange> = Vec::new();
let options = AnalyzerOptions::default()
.with_file_path(file_path.clone())
.with_configuration(
AnalyzerConfiguration::default()
.with_jsx_runtime(JsxRuntime::ReactClassic)
.with_jsx_factory(Some("h".into()))
.with_jsx_fragment_factory(Some("Fragment".into())),
);
let rule_filter = RuleFilter::Rule("correctness", "noUnusedImports");

let services = JsAnalyzerServices::default();

analyze(
&parsed.tree(),
AnalysisFilter {
enabled_rules: Some(slice::from_ref(&rule_filter)),
..AnalysisFilter::default()
},
&options,
&[],
services,
|signal| {
if let Some(diag) = signal.diagnostic() {
error_ranges.push(diag.location().span.unwrap());
let error = diag
.with_severity(Severity::Warning)
.with_file_path(FILENAME)
.with_file_source_code(SOURCE);
let text = print_diagnostic_to_string(&error);
eprintln!("{text}");
}

ControlFlow::<Never>::Continue(())
},
);

// Should not report any errors because h and Fragment are used as JSX factory functions
assert_eq!(error_ranges.as_slice(), &[]);
}
26 changes: 25 additions & 1 deletion crates/biome_js_analyze/tests/spec_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,31 @@ pub(crate) fn analyze_and_snap(
let parsed = parse(input_code, source_type, parser_options);
let root = parsed.tree();

let options = create_analyzer_options::<JsLanguage>(input_file, &mut diagnostics);
let mut options = create_analyzer_options::<JsLanguage>(input_file, &mut diagnostics);

// Query tsconfig.json for JSX factory settings if jsx_runtime is ReactClassic
// and the factory settings are not already set
use biome_analyze::options::JsxRuntime;
if options.jsx_runtime() == Some(JsxRuntime::ReactClassic) {
if options.jsx_factory().is_none() {
let factory = project_layout
.query_tsconfig_for_path(input_file, |tsconfig| {
tsconfig.jsx_factory_identifier().map(|s| s.to_string())
})
.flatten();
options.set_jsx_factory(factory.map(|s| s.into()));
}
if options.jsx_fragment_factory().is_none() {
let fragment_factory = project_layout
.query_tsconfig_for_path(input_file, |tsconfig| {
tsconfig
.jsx_fragment_factory_identifier()
.map(|s| s.to_string())
})
.flatten();
options.set_jsx_fragment_factory(fragment_factory.map(|s| s.into()));
}
}

let needs_module_graph = NeedsModuleGraph::new(filter.enabled_rules).compute();
let module_graph = if needs_module_graph {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { h, Fragment } from 'preact';

function App() {
return (
<>
<div>Hello World</div>
</>
);
}

Loading
Loading