diff --git a/Cargo.lock b/Cargo.lock index 01fa58177044..697465c7e801 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -208,6 +208,7 @@ dependencies = [ "biome_graphql_syntax", "biome_js_analyze", "biome_js_formatter", + "biome_js_parser", "biome_js_syntax", "biome_json_analyze", "biome_json_formatter", @@ -798,6 +799,7 @@ version = "0.5.7" dependencies = [ "biome_console", "biome_diagnostics", + "biome_js_factory", "biome_js_parser", "biome_js_syntax", "biome_rowan", diff --git a/crates/biome_analyze/src/context.rs b/crates/biome_analyze/src/context.rs index b4ff5d224862..c96073b65617 100644 --- a/crates/biome_analyze/src/context.rs +++ b/crates/biome_analyze/src/context.rs @@ -21,6 +21,8 @@ where options: &'a R::Options, preferred_quote: &'a PreferredQuote, jsx_runtime: Option, + jsx_factory: Option<&'a str>, + jsx_fragment_factory: Option<&'a str>, } impl<'a, R> RuleContext<'a, R> @@ -37,6 +39,8 @@ where options: &'a R::Options, preferred_quote: &'a PreferredQuote, jsx_runtime: Option, + jsx_factory: Option<&'a str>, + jsx_fragment_factory: Option<&'a str>, ) -> Result { let rule_key = RuleKey::rule::(); Ok(Self { @@ -49,6 +53,8 @@ where options, preferred_quote, jsx_runtime, + jsx_factory, + jsx_fragment_factory, }) } @@ -139,6 +145,16 @@ where self.jsx_runtime.expect("jsx_runtime should be provided") } + /// Returns the JSX factory in use. + pub fn jsx_factory(&self) -> Option<&str> { + self.jsx_factory + } + + /// Returns the JSX fragment factory in use. + 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.contains(&text) diff --git a/crates/biome_analyze/src/options.rs b/crates/biome_analyze/src/options.rs index 52c13a52978e..30356821249b 100644 --- a/crates/biome_analyze/src/options.rs +++ b/crates/biome_analyze/src/options.rs @@ -3,6 +3,7 @@ use rustc_hash::FxHashMap; use crate::{FixKind, Rule, RuleKey}; use std::any::{Any, TypeId}; use std::fmt::Debug; +use std::ops::Deref; use std::path::PathBuf; /// A convenient new type data structure to store the options that belong to a rule @@ -51,6 +52,33 @@ impl AnalyzerRules { } } +/// Jsx factory namespace +#[derive(Debug)] +pub struct JsxFactory(Box); + +impl Deref for JsxFactory { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl JsxFactory { + /// Create a new [`JsxFactory`] from a factory name + // invariant: factory should only be an identifier + pub fn new(factory: String) -> Self { + debug_assert!(!factory.contains(['.', '[', ']'])); + Self(factory.into_boxed_str()) + } +} + +impl From for JsxFactory { + fn from(s: String) -> Self { + Self::new(s) + } +} + /// A data structured derived from the `biome.json` file #[derive(Debug, Default)] pub struct AnalyzerConfiguration { @@ -67,6 +95,16 @@ pub struct AnalyzerConfiguration { /// Indicates the type of runtime or transformation used for interpreting JSX. pub jsx_runtime: Option, + + /// Indicates the name of the factory function used to create JSX elements. + /// + /// Ignored if `jsx_runtime` is not set to [`JsxRuntime::ReactClassic`]. + pub jsx_factory: Option, + + /// Indicates the name of the factory function used to create JSX fragments. + /// + /// Ignored if `jsx_runtime` is not set to [`JsxRuntime::ReactClassic`]. + pub jsx_fragment_factory: Option, } /// A set of information useful to the analyzer infrastructure @@ -92,6 +130,14 @@ impl AnalyzerOptions { self.configuration.jsx_runtime } + pub fn jsx_factory(&self) -> Option<&str> { + self.configuration.jsx_factory.as_deref() + } + + pub fn jsx_fragment_factory(&self) -> Option<&str> { + self.configuration.jsx_fragment_factory.as_deref() + } + pub fn rule_options(&self) -> Option where R: Rule + 'static, diff --git a/crates/biome_analyze/src/registry.rs b/crates/biome_analyze/src/registry.rs index dd4121be19ac..3300cbe6fa96 100644 --- a/crates/biome_analyze/src/registry.rs +++ b/crates/biome_analyze/src/registry.rs @@ -400,6 +400,8 @@ impl RegistryRule { let globals = params.options.globals(); let preferred_quote = params.options.preferred_quote(); let jsx_runtime = params.options.jsx_runtime(); + let jsx_factory = params.options.jsx_factory(); + let jsx_fragment_factory = params.options.jsx_fragment_factory(); let options = params.options.rule_options::().unwrap_or_default(); let ctx = match RuleContext::new( &query_result, @@ -410,6 +412,8 @@ impl RegistryRule { &options, preferred_quote, jsx_runtime, + jsx_factory, + jsx_fragment_factory, ) { Ok(ctx) => ctx, Err(error) => return Err(error), diff --git a/crates/biome_analyze/src/signals.rs b/crates/biome_analyze/src/signals.rs index d69e97b77f8b..21b67a64e31e 100644 --- a/crates/biome_analyze/src/signals.rs +++ b/crates/biome_analyze/src/signals.rs @@ -357,6 +357,8 @@ where &options, preferred_quote, self.options.jsx_runtime(), + self.options.jsx_factory(), + self.options.jsx_fragment_factory(), ) .ok()?; @@ -388,6 +390,8 @@ where &options, self.options.preferred_quote(), self.options.jsx_runtime(), + self.options.jsx_factory(), + self.options.jsx_fragment_factory(), ) .ok(); if let Some(ctx) = ctx { @@ -434,6 +438,8 @@ where &options, self.options.preferred_quote(), self.options.jsx_runtime(), + self.options.jsx_factory(), + self.options.jsx_fragment_factory(), ) .ok(); if let Some(ctx) = ctx { diff --git a/crates/biome_configuration/Cargo.toml b/crates/biome_configuration/Cargo.toml index b4ae45f6ac84..4a0a26829dea 100644 --- a/crates/biome_configuration/Cargo.toml +++ b/crates/biome_configuration/Cargo.toml @@ -26,6 +26,7 @@ biome_graphql_analyze = { workspace = true } biome_graphql_syntax = { workspace = true } biome_js_analyze = { workspace = true } biome_js_formatter = { workspace = true, features = ["serde"] } +biome_js_parser = { workspace = true } biome_js_syntax = { workspace = true, features = ["schema"] } biome_json_analyze = { workspace = true } biome_json_formatter = { workspace = true, features = ["serde"] } diff --git a/crates/biome_configuration/src/javascript/mod.rs b/crates/biome_configuration/src/javascript/mod.rs index 9ac646202d41..5f94a1890e53 100644 --- a/crates/biome_configuration/src/javascript/mod.rs +++ b/crates/biome_configuration/src/javascript/mod.rs @@ -2,13 +2,16 @@ mod formatter; use std::str::FromStr; +use biome_console::markup; use biome_deserialize::StringSet; use biome_deserialize_macros::{Deserializable, Merge, Partial}; +use biome_js_parser::{parse_module, JsParserOptions}; +use biome_rowan::AstNode; use bpaf::Bpaf; pub use formatter::{ partial_javascript_formatter, JavascriptFormatter, PartialJavascriptFormatter, }; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; /// A set of options applied to the JavaScript files #[derive(Clone, Debug, Default, Deserialize, Eq, Partial, PartialEq, Serialize)] @@ -42,6 +45,20 @@ pub struct JavascriptConfiguration { #[partial(bpaf(hide))] pub jsx_runtime: JsxRuntime, + /// Indicates the name of the factory function used to create JSX elements. + #[partial( + bpaf(hide), + serde(deserialize_with = "deserialize_optional_jsx_factory_from_string") + )] + pub jsx_factory: JsxFactory, + + /// Indicates the name of the factory function used to create JSX fragments. + #[partial( + bpaf(hide), + serde(deserialize_with = "deserialize_optional_jsx_factory_from_string") + )] + pub jsx_fragment_factory: JsxFactory, + #[partial(type, bpaf(external(partial_javascript_organize_imports), optional))] pub organize_imports: JavascriptOrganizeImports, } @@ -100,6 +117,93 @@ impl FromStr for JsxRuntime { } } +fn deserialize_optional_jsx_factory_from_string<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + match parse_jsx_factory(&s) { + Some(factory) => Ok(Some(factory)), + None => Err(serde::de::Error::custom(format!( + "expected valid identifier or qualified name, but received {s}" + ))), + } +} + +fn parse_jsx_factory(value: &str) -> Option { + use biome_js_syntax::*; + let syntax = parse_module(value, JsParserOptions::default()); + let item = syntax.try_tree()?.items().into_iter().next()?; + if let AnyJsModuleItem::AnyJsStatement(stmt) = item { + let expr = JsExpressionStatement::cast_ref(stmt.syntax())? + .expression() + .ok()?; + if let AnyJsExpression::JsStaticMemberExpression(member) = expr { + let mut expr = member.object().ok(); + while let Some(e) = expr { + if let Some(ident) = JsIdentifierExpression::cast_ref(e.syntax()) { + return Some(JsxFactory(ident.text().clone())); + } else if let Some(member) = JsStaticMemberExpression::cast_ref(e.syntax()) { + expr = member.object().ok(); + } else { + break; + } + } + } else if let AnyJsExpression::JsIdentifierExpression(ident) = expr { + return Some(JsxFactory(ident.text().clone())); + } + } + + None +} + +#[derive(Bpaf, Clone, Debug, Deserialize, Eq, Merge, PartialEq, Serialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase")] +pub struct JsxFactory(pub String); + +impl Default for JsxFactory { + fn default() -> Self { + Self("React".to_string()) + } +} + +impl JsxFactory { + pub fn into_string(self) -> String { + self.0 + } +} + +impl biome_deserialize::Deserializable for JsxFactory { + fn deserialize( + value: &impl biome_deserialize::DeserializableValue, + name: &str, + diagnostics: &mut Vec, + ) -> Option { + let factory = biome_deserialize::Text::deserialize(value, name, diagnostics)?; + parse_jsx_factory(factory.text()).or_else(|| { + diagnostics.push(biome_deserialize::DeserializationDiagnostic::new( + markup!( + "Incorrect value, expected "{"identifier"}" or "{"qualified name"}", but received "{format_args!("{}", factory.text())}"." + ), + ).with_range(value.range())); + None + }) + } +} + +impl FromStr for JsxFactory { + type Err = String; + fn from_str(s: &str) -> Result { + let factory = parse_jsx_factory(s).ok_or_else(|| { + format!("expected valid identifier or qualified name, but received {s}") + })?; + Ok(factory) + } +} + /// Linter options specific to the JavaScript linter #[derive(Clone, Debug, Deserialize, Eq, Partial, PartialEq, Serialize)] #[partial(derive(Bpaf, Clone, Deserializable, Eq, Merge, PartialEq))] diff --git a/crates/biome_js_analyze/src/lint/correctness/no_undeclared_variables.rs b/crates/biome_js_analyze/src/lint/correctness/no_undeclared_variables.rs index b70a1776502b..75fb561a2b08 100644 --- a/crates/biome_js_analyze/src/lint/correctness/no_undeclared_variables.rs +++ b/crates/biome_js_analyze/src/lint/correctness/no_undeclared_variables.rs @@ -1,6 +1,7 @@ use crate::globals::{is_js_global, is_ts_global}; use crate::services::semantic::SemanticServices; use biome_analyze::context::RuleContext; +use biome_analyze::options::JsxRuntime; use biome_analyze::{declare_lint_rule, Rule, RuleDiagnostic, RuleSource}; use biome_console::markup; use biome_js_syntax::{ @@ -8,6 +9,8 @@ use biome_js_syntax::{ }; use biome_rowan::AstNode; +const REACT_JSX_FACTORY: &str = "React"; + declare_lint_rule! { /// Prevents the usage of variables that haven't been declared inside the document. /// @@ -49,47 +52,70 @@ impl Rule for NoUndeclaredVariables { ctx.query() .all_unresolved_references() .filter_map(|reference| { - let identifier = reference.tree(); - let under_as_expression = identifier - .parent::() - .and_then(|ty| ty.parent::()) - .is_some(); + if let Some(identifier) = reference.as_js_identifier() { + let under_as_expression = identifier + .parent::() + .and_then(|ty| ty.parent::()) + .is_some(); - let token = identifier.value_token().ok()?; - let text = token.text_trimmed(); + let token = identifier.value_token().ok()?; + let text = token.text_trimmed(); - let source_type = ctx.source_type::(); + let source_type = ctx.source_type::(); - if ctx.is_global(text) { - return None; - } + if ctx.is_global(text) { + return None; + } - // Typescript Const Assertion - if text == "const" && under_as_expression { - return None; - } + // Typescript Const Assertion + if text == "const" && under_as_expression { + return None; + } - // arguments object within non-arrow functions - if text == "arguments" { - let is_in_non_arrow_function = - identifier.syntax().ancestors().any(|ancestor| { - !matches!( - AnyJsFunction::cast(ancestor), - None | Some(AnyJsFunction::JsArrowFunctionExpression(_)) - ) - }); - if is_in_non_arrow_function { + // arguments object within non-arrow functions + if text == "arguments" { + let is_in_non_arrow_function = + identifier.syntax().ancestors().any(|ancestor| { + !matches!( + AnyJsFunction::cast(ancestor), + None | Some(AnyJsFunction::JsArrowFunctionExpression(_)) + ) + }); + if is_in_non_arrow_function { + return None; + } + } + + if is_global(text, source_type) { return None; } - } - if is_global(text, source_type) { - return None; - } + let span = token.text_trimmed_range(); + let text = text.to_string(); + Some((span, text)) + } else if ctx.jsx_runtime() == JsxRuntime::ReactClassic { + if let Some(jsx_like) = reference.as_jsx_like() { + let jsx_factory = ctx.jsx_factory()?; + if jsx_factory == REACT_JSX_FACTORY { + return None; + } + let span = jsx_like.name_value_token()?.text_trimmed_range(); + return Some((span, jsx_factory.to_string())); + } - let span = token.text_trimmed_range(); - let text = text.to_string(); - Some((span, text)) + if let Some(jsx_fragment) = reference.as_jsx_fragment() { + let jsx_fragment_factory = ctx.jsx_fragment_factory()?; + if jsx_fragment_factory == REACT_JSX_FACTORY { + return None; + } + let span = jsx_fragment.l_angle_token().ok()?.text_trimmed_range(); + return Some((span, jsx_fragment_factory.to_string())); + } + + None + } else { + None + } }) .collect() } diff --git a/crates/biome_js_analyze/src/services/semantic.rs b/crates/biome_js_analyze/src/services/semantic.rs index 519aa45f1f97..d2b43967cb8d 100644 --- a/crates/biome_js_analyze/src/services/semantic.rs +++ b/crates/biome_js_analyze/src/services/semantic.rs @@ -2,7 +2,9 @@ use biome_analyze::{ AddVisitor, FromServices, MissingServicesDiagnostic, Phase, Phases, QueryKey, QueryMatch, Queryable, RuleKey, ServiceBag, SyntaxVisitor, Visitor, VisitorContext, VisitorFinishContext, }; -use biome_js_semantic::{SemanticEventExtractor, SemanticModel, SemanticModelBuilder}; +use biome_js_semantic::{ + SemanticEventExtractor, SemanticEventExtractorContext, SemanticModel, SemanticModelBuilder, +}; use biome_js_syntax::{AnyJsRoot, JsLanguage, JsSyntaxNode, TextRange, WalkEvent}; use biome_rowan::AstNode; @@ -103,11 +105,17 @@ impl SemanticModelBuilderVisitor { impl Visitor for SemanticModelBuilderVisitor { type Language = JsLanguage; - fn visit(&mut self, event: &WalkEvent, _ctx: VisitorContext) { + fn visit(&mut self, event: &WalkEvent, ctx: VisitorContext) { match event { WalkEvent::Enter(node) => { self.builder.push_node(node); - self.extractor.enter(node); + self.extractor.enter( + node, + &SemanticEventExtractorContext { + jsx_factory: ctx.options.jsx_factory(), + jsx_fragment_factory: ctx.options.jsx_fragment_factory(), + }, + ); } WalkEvent::Leave(node) => { self.extractor.leave(node); diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/jsx.jsx b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/jsx.jsx new file mode 100644 index 000000000000..a89dc53b23f1 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/jsx.jsx @@ -0,0 +1,8 @@ +function App({ children }) { + const h = () => {}; + return
{children}
; +} + +<>; +; +abc; diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/jsx.jsx.snap b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/jsx.jsx.snap new file mode 100644 index 000000000000..9a09513f1d4b --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/jsx.jsx.snap @@ -0,0 +1,69 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: jsx.jsx +--- +# Input +```jsx +function App({ children }) { + const h = () => {}; + return
{children}
; +} + +<>; +; +abc; + +``` + +# Diagnostics +``` +jsx.jsx:6:1 lint/correctness/noUndeclaredVariables ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The Fragment variable is undeclared. + + 4 │ } + 5 │ + > 6 │ <>; + │ ^ + 7 │ ; + 8 │ abc; + + i By default, Biome recognizes browser and Node.js globals. + You can ignore more globals using the javascript.globals configuration. + + +``` + +``` +jsx.jsx:7:2 lint/correctness/noUndeclaredVariables ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The h variable is undeclared. + + 6 │ <>; + > 7 │ ; + │ ^^^ + 8 │ abc; + 9 │ + + i By default, Biome recognizes browser and Node.js globals. + You can ignore more globals using the javascript.globals configuration. + + +``` + +``` +jsx.jsx:8:2 lint/correctness/noUndeclaredVariables ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The h variable is undeclared. + + 6 │ <>; + 7 │ ; + > 8 │ abc; + │ ^^^ + 9 │ + + i By default, Biome recognizes browser and Node.js globals. + You can ignore more globals using the javascript.globals configuration. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/jsx.options.json b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/jsx.options.json new file mode 100644 index 000000000000..d46ac3c2b1e0 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/jsx.options.json @@ -0,0 +1,7 @@ +{ + "javascript": { + "jsxRuntime": "reactClassic", + "jsxFactory": "h", + "jsxFragmentFactory": "Fragment" + } +} diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/react-jsx.jsx b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/react-jsx.jsx new file mode 100644 index 000000000000..8e30626e4a81 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/react-jsx.jsx @@ -0,0 +1,3 @@ +<> +
+; diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/react-jsx.jsx.snap b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/react-jsx.jsx.snap new file mode 100644 index 000000000000..c11316fd94dd --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/react-jsx.jsx.snap @@ -0,0 +1,11 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: react-jsx.jsx +--- +# Input +```jsx +<> +
+; + +``` diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/react-jsx.options.json b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/react-jsx.options.json new file mode 100644 index 000000000000..25b9637932e1 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/react-jsx.options.json @@ -0,0 +1,5 @@ +{ + "javascript": { + "jsxRuntime": "reactClassic" + } +} diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/scripts/print-changelog.sh b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/scripts/print-changelog.sh new file mode 100644 index 000000000000..aebf9beb2f6f --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/scripts/print-changelog.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -eu + +# Print a changelog section (default: first section). + +VERSION='' + +if test -n "${1:-}" && grep -Eq "^## $1($| )" CHANGELOG.md; then + # The specified version has a dedicated section in the changelog + VERSION="$1" +fi + +# print Changelog of $VERSION +awk -v version="$VERSION" '/^## / { if (p) { exit }; if (version == "" || $2 == version) { p=1; next} } p' CHANGELOG.md diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/scripts/update-manifests.mjs b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/scripts/update-manifests.mjs new file mode 100644 index 000000000000..3ef8acf5c30a --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/scripts/update-manifests.mjs @@ -0,0 +1,106 @@ +import * as fs from "node:fs"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { format } from "node:util"; + +const REPO_ROOT = resolve(fileURLToPath(import.meta.url), "../.."); +const PACKAGES_ROOT = resolve(REPO_ROOT, "packages/@biomejs"); +const BIOME_LIB_PATH = resolve(PACKAGES_ROOT, "biome"); +const MANIFEST_PATH = resolve(BIOME_LIB_PATH, "package.json"); + +const PLATFORMS = ["win32-%s", "darwin-%s", "linux-%s", "linux-%s-musl"]; +const ARCHITECTURES = ["x64", "arm64"]; +const WASM_TARGETS = ["bundler", "nodejs", "web"]; + + +const rootManifest = JSON.parse( + fs.readFileSync(MANIFEST_PATH).toString("utf-8"), +); + + +for (const platform of PLATFORMS) { + for (const arch of ARCHITECTURES) { + updateOptionalDependencies(platform, arch); + } +} + +for (const target of WASM_TARGETS) { + updateWasmPackage(target); +} + + +function getName(platform, arch, prefix = "cli") { + return format(`${prefix}-${platform}`, arch); +} + +function updateOptionalDependencies(platform, arch) { + const os = platform.split("-")[0]; + const buildName = getName(platform, arch); + const packageRoot = resolve(PACKAGES_ROOT, buildName); + const packageName = `@biomejs/${buildName}`; + + // Update the package.json manifest + const { version, license, repository, engines, homepage } = rootManifest; + + const manifest = JSON.stringify( + { + name: packageName, + version, + license, + repository: { + ...repository, + directory: repository.directory + "/" + buildName + }, + engines, + homepage, + os: [os], + cpu: [arch], + libc: + os === "linux" + ? packageName.endsWith("musl") + ? ["musl"] + : ["glibc"] + : undefined, + }, + null, + 2, + ); + + const manifestPath = resolve(packageRoot, "package.json"); + console.log(`Update manifest ${manifestPath}`); + fs.writeFileSync(manifestPath, manifest); + + // Copy the CLI binary + const ext = os === "win32" ? ".exe" : ""; + const binarySource = resolve( + REPO_ROOT, + `${getName(platform, arch, "biome")}${ext}`, + ); + const binaryTarget = resolve(packageRoot, `biome${ext}`); + + if (fs.existsSync(binaryTarget)) { + console.log(`Copy binary ${binaryTarget}`); + + fs.copyFileSync(binarySource, binaryTarget); + fs.chmodSync(binaryTarget, 0o755); + } +} + +function updateWasmPackage(target) { + const packageName = `@biomejs/wasm-${target}`; + const packageRoot = resolve(PACKAGES_ROOT, `wasm-${target}`); + + const manifestPath = resolve(packageRoot, "package.json"); + const manifest = JSON.parse(fs.readFileSync(manifestPath).toString("utf-8")); + + const { version, repository } = rootManifest; + manifest.name = packageName; + manifest.version = version; + manifest.repository = { + ...repository, + directory: repository.directory + `/wasm-${target}` + } + + console.log(`Update manifest ${manifestPath}`); + fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); +} diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/invalid-jsx-factory-self.jsx b/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/invalid-jsx-factory-self.jsx new file mode 100644 index 000000000000..cbc7718f7c1d --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/invalid-jsx-factory-self.jsx @@ -0,0 +1,3 @@ +import { h, Fragment } from "preact"; + +
; diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/invalid-jsx-factory-self.jsx.snap b/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/invalid-jsx-factory-self.jsx.snap new file mode 100644 index 000000000000..22f2c8c6b206 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/invalid-jsx-factory-self.jsx.snap @@ -0,0 +1,31 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid-jsx-factory-self.jsx +--- +# Input +```jsx +import { h, Fragment } from "preact"; + +
; + +``` + +# Diagnostics +``` +invalid-jsx-factory-self.jsx:1:13 lint/correctness/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + > 1 │ import { h, Fragment } from "preact"; + │ ^^^^^^^^ + 2 │ + 3 │
; + + i Unused imports might be the result of an incomplete refactoring. + + i Safe fix: Remove the unused import. + + 1 │ import·{·h,·Fragment·}·from·"preact"; + │ --------- + +``` diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/invalid-jsx-factory-self.options.json b/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/invalid-jsx-factory-self.options.json new file mode 100644 index 000000000000..0efcb54f81a5 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/invalid-jsx-factory-self.options.json @@ -0,0 +1,14 @@ +{ + "linter": { + "rules": { + "correctness": { + "noUnusedImports": "error" + } + } + }, + "javascript": { + "jsxRuntime": "reactClassic", + "jsxFactory": "h", + "jsxFragmentFactory": "Fragment" + } +} diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/invalid-jsx-factory.jsx b/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/invalid-jsx-factory.jsx new file mode 100644 index 000000000000..af2ad2466f59 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/invalid-jsx-factory.jsx @@ -0,0 +1,3 @@ +import { h, Fragment } from "preact"; + +
; diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/invalid-jsx-factory.jsx.snap b/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/invalid-jsx-factory.jsx.snap new file mode 100644 index 000000000000..d3b00b792c65 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/invalid-jsx-factory.jsx.snap @@ -0,0 +1,31 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid-jsx-factory.jsx +--- +# Input +```jsx +import { h, Fragment } from "preact"; + +
; + +``` + +# Diagnostics +``` +invalid-jsx-factory.jsx:1:13 lint/correctness/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + > 1 │ import { h, Fragment } from "preact"; + │ ^^^^^^^^ + 2 │ + 3 │
; + + i Unused imports might be the result of an incomplete refactoring. + + i Safe fix: Remove the unused import. + + 1 │ import·{·h,·Fragment·}·from·"preact"; + │ --------- + +``` diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/invalid-jsx-factory.options.json b/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/invalid-jsx-factory.options.json new file mode 100644 index 000000000000..0efcb54f81a5 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/invalid-jsx-factory.options.json @@ -0,0 +1,14 @@ +{ + "linter": { + "rules": { + "correctness": { + "noUnusedImports": "error" + } + } + }, + "javascript": { + "jsxRuntime": "reactClassic", + "jsxFactory": "h", + "jsxFragmentFactory": "Fragment" + } +} diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/invalid-jsx-fragment-factory.jsx b/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/invalid-jsx-fragment-factory.jsx new file mode 100644 index 000000000000..486ca9310445 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/invalid-jsx-fragment-factory.jsx @@ -0,0 +1,3 @@ +import { h, Fragment } from "preact"; + +<>; diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/invalid-jsx-fragment-factory.jsx.snap b/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/invalid-jsx-fragment-factory.jsx.snap new file mode 100644 index 000000000000..9b79bded4c38 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/invalid-jsx-fragment-factory.jsx.snap @@ -0,0 +1,31 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid-jsx-fragment-factory.jsx +--- +# Input +```jsx +import { h, Fragment } from "preact"; + +<>; + +``` + +# Diagnostics +``` +invalid-jsx-fragment-factory.jsx:1:10 lint/correctness/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + > 1 │ import { h, Fragment } from "preact"; + │ ^ + 2 │ + 3 │ <>; + + i Unused imports might be the result of an incomplete refactoring. + + i Safe fix: Remove the unused import. + + 1 │ import·{·h,·Fragment·}·from·"preact"; + │ --- + +``` diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/invalid-jsx-fragment-factory.options.json b/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/invalid-jsx-fragment-factory.options.json new file mode 100644 index 000000000000..0efcb54f81a5 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/invalid-jsx-fragment-factory.options.json @@ -0,0 +1,14 @@ +{ + "linter": { + "rules": { + "correctness": { + "noUnusedImports": "error" + } + } + }, + "javascript": { + "jsxRuntime": "reactClassic", + "jsxFactory": "h", + "jsxFragmentFactory": "Fragment" + } +} diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/valid-jsx-factory-namespace.jsx b/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/valid-jsx-factory-namespace.jsx new file mode 100644 index 000000000000..b552fe87ac23 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/valid-jsx-factory-namespace.jsx @@ -0,0 +1,3 @@ +import * as preact from "preact"; + +
; diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/valid-jsx-factory-namespace.jsx.snap b/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/valid-jsx-factory-namespace.jsx.snap new file mode 100644 index 000000000000..7fc6b538a554 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/valid-jsx-factory-namespace.jsx.snap @@ -0,0 +1,12 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 113 +expression: jsx-factory-namespace.jsx +--- +# Input +```jsx +import * as preact from "preact"; + +
; + +``` diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/valid-jsx-factory-namespace.options.json b/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/valid-jsx-factory-namespace.options.json new file mode 100644 index 000000000000..adeb5043f29e --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedImports/valid-jsx-factory-namespace.options.json @@ -0,0 +1,14 @@ +{ + "linter": { + "rules": { + "correctness": { + "noUnusedImports": "error" + } + } + }, + "javascript": { + "jsxRuntime": "reactClassic", + "jsxFactory": "preact.h", + "jsxFragmentFactory": "preact.Fragment" + } +} diff --git a/crates/biome_js_semantic/Cargo.toml b/crates/biome_js_semantic/Cargo.toml index a7a4ed94c3e8..969e8294e85f 100644 --- a/crates/biome_js_semantic/Cargo.toml +++ b/crates/biome_js_semantic/Cargo.toml @@ -11,10 +11,11 @@ repository.workspace = true version = "0.5.7" [dependencies] -biome_js_syntax = { workspace = true } -biome_rowan = { workspace = true } -rust-lapper = "1.1.0" -rustc-hash = { workspace = true } +biome_js_factory = { workspace = true } +biome_js_syntax = { workspace = true } +biome_rowan = { workspace = true } +rust-lapper = "1.1.0" +rustc-hash = { workspace = true } [dev-dependencies] biome_console = { path = "../biome_console" } diff --git a/crates/biome_js_semantic/src/events.rs b/crates/biome_js_semantic/src/events.rs index bed7ce3f0efb..6393f839925a 100644 --- a/crates/biome_js_semantic/src/events.rs +++ b/crates/biome_js_semantic/src/events.rs @@ -1,5 +1,6 @@ //! Events emitted by the [SemanticEventExtractor] which are then constructed into the Semantic Model +use biome_js_factory::make; use biome_js_syntax::binding_ext::{AnyJsBindingDeclaration, AnyJsIdentifierBinding}; use biome_js_syntax::{ inner_string_text, AnyJsIdentifierUsage, JsDirective, JsLanguage, JsSyntaxKind, JsSyntaxNode, @@ -132,9 +133,10 @@ impl SemanticEvent { /// use biome_js_semantic::*; /// let tree = parse("let a = 1", JsFileSource::js_script(), JsParserOptions::default()); /// let mut extractor = SemanticEventExtractor::default(); +/// let ctx = SemanticEventExtractorContext::default(); /// for e in tree.syntax().preorder() { /// match e { -/// WalkEvent::Enter(node) => extractor.enter(&node), +/// WalkEvent::Enter(node) => extractor.enter(&node, &ctx), /// WalkEvent::Leave(node) => extractor.leave(&node), /// _ => {} /// } @@ -282,10 +284,18 @@ struct Scope { is_in_strict_mode: bool, } +#[derive(Default)] +pub struct SemanticEventExtractorContext<'a> { + /// The factory used to create JSX elements. + pub jsx_factory: Option<&'a str>, + /// The factory used to create JSX fragments. + pub jsx_fragment_factory: Option<&'a str>, +} + impl SemanticEventExtractor { /// See [SemanticEvent] for a more detailed description of which events [JsSyntaxNode] generates. #[inline] - pub fn enter(&mut self, node: &JsSyntaxNode) { + pub fn enter(&mut self, node: &JsSyntaxNode, ctx: &SemanticEventExtractorContext) { // IMPORTANT: If you push a scope for a given node type, don't forget to // update `Self::leave`. You should also edit [SemanticModelBuilder::push_node]. match node.kind() { @@ -296,6 +306,26 @@ impl SemanticEventExtractor { self.enter_identifier_binding(&AnyJsIdentifierBinding::unwrap_cast(node.clone())); } + JSX_OPENING_ELEMENT | JSX_SELF_CLOSING_ELEMENT => { + if let Some(factory) = ctx.jsx_factory.as_ref() { + let ident = make::ident(factory); + self.push_reference( + BindingName::Value(ident.token_text_trimmed()), + Reference::Read(node.text_trimmed_range()), + ); + } + } + + JSX_OPENING_FRAGMENT => { + if let Some(factory) = ctx.jsx_fragment_factory.as_ref() { + let ident = make::ident(factory); + self.push_reference( + BindingName::Value(ident.token_text_trimmed()), + Reference::Read(node.text_trimmed_range()), + ); + } + } + JS_REFERENCE_IDENTIFIER | JSX_REFERENCE_IDENTIFIER | JS_IDENTIFIER_ASSIGNMENT => { self.enter_identifier_usage(AnyJsIdentifierUsage::unwrap_cast(node.clone())); } @@ -1128,7 +1158,7 @@ impl Iterator for SemanticEventIterator { use biome_js_syntax::WalkEvent::*; match self.iter.next() { Some(Enter(node)) => { - self.extractor.enter(&node); + self.extractor.enter(&node, &Default::default()); } Some(Leave(node)) => { self.extractor.leave(&node); diff --git a/crates/biome_js_semantic/src/semantic_model.rs b/crates/biome_js_semantic/src/semantic_model.rs index 4b79ed0b0c30..0d9a6c4fb65c 100644 --- a/crates/biome_js_semantic/src/semantic_model.rs +++ b/crates/biome_js_semantic/src/semantic_model.rs @@ -11,7 +11,7 @@ mod scope; #[cfg(test)] mod tests; -use crate::{SemanticEvent, SemanticEventExtractor}; +use crate::{SemanticEvent, SemanticEventExtractor, SemanticEventExtractorContext}; use biome_js_syntax::{ AnyJsExpression, AnyJsRoot, JsIdentifierAssignment, JsIdentifierBinding, JsLanguage, JsReferenceIdentifier, JsSyntaxKind, JsSyntaxNode, JsxReferenceIdentifier, TextRange, TextSize, @@ -42,6 +42,10 @@ pub use scope::*; pub struct SemanticModelOptions { /// All the allowed globals names pub globals: FxHashSet, + /// The JSX factory name + pub jsx_factory: Option, + /// The JSX fragment factory name + pub jsx_fragment_factory: Option, } /// Build the complete [SemanticModel] of a parsed file. @@ -50,18 +54,27 @@ pub fn semantic_model(root: &AnyJsRoot, options: SemanticModelOptions) -> Semant let mut extractor = SemanticEventExtractor::default(); let mut builder = SemanticModelBuilder::new(root.clone()); - let SemanticModelOptions { globals } = options; + let SemanticModelOptions { + globals, + jsx_factory, + jsx_fragment_factory, + } = options; for global in globals { builder.push_global(global); } + let ctx = SemanticEventExtractorContext { + jsx_factory: jsx_factory.as_deref(), + jsx_fragment_factory: jsx_fragment_factory.as_deref(), + }; + let root = root.syntax(); for node in root.preorder() { match node { biome_js_syntax::WalkEvent::Enter(node) => { builder.push_node(&node); - extractor.enter(&node); + extractor.enter(&node, &ctx); } biome_js_syntax::WalkEvent::Leave(node) => extractor.leave(&node), } diff --git a/crates/biome_js_semantic/src/semantic_model/builder.rs b/crates/biome_js_semantic/src/semantic_model/builder.rs index ac8c76dba880..2766659bda33 100644 --- a/crates/biome_js_semantic/src/semantic_model/builder.rs +++ b/crates/biome_js_semantic/src/semantic_model/builder.rs @@ -58,7 +58,10 @@ impl SemanticModelBuilder { | JSX_REFERENCE_IDENTIFIER | TS_TYPE_PARAMETER_NAME | TS_LITERAL_ENUM_MEMBER_NAME - | JS_IDENTIFIER_ASSIGNMENT => { + | JS_IDENTIFIER_ASSIGNMENT + | JSX_OPENING_ELEMENT + | JSX_OPENING_FRAGMENT + | JSX_SELF_CLOSING_ELEMENT => { self.binding_node_by_start .insert(node.text_trimmed_range().start(), node.clone()); } diff --git a/crates/biome_js_semantic/src/semantic_model/reference.rs b/crates/biome_js_semantic/src/semantic_model/reference.rs index 105d89813584..b02a36ba5b2d 100644 --- a/crates/biome_js_semantic/src/semantic_model/reference.rs +++ b/crates/biome_js_semantic/src/semantic_model/reference.rs @@ -1,4 +1,7 @@ -use biome_js_syntax::{AnyJsFunction, AnyJsIdentifierUsage, JsCallExpression}; +use biome_js_syntax::{ + jsx_ext::AnyJsxElement, AnyJsFunction, AnyJsIdentifierUsage, JsCallExpression, + JsxOpeningFragment, +}; use super::*; use std::rc::Rc; @@ -189,8 +192,16 @@ impl UnresolvedReference { &self.data.binding_node_by_start[&reference.range.start()] } - pub fn tree(&self) -> AnyJsIdentifierUsage { - AnyJsIdentifierUsage::unwrap_cast(self.syntax().clone()) + pub fn as_js_identifier(&self) -> Option { + AnyJsIdentifierUsage::try_cast(self.syntax().clone()).ok() + } + + pub fn as_jsx_like(&self) -> Option { + AnyJsxElement::try_cast(self.syntax().clone()).ok() + } + + pub fn as_jsx_fragment(&self) -> Option { + JsxOpeningFragment::try_cast(self.syntax().clone()).ok() } pub fn range(&self) -> TextRange { diff --git a/crates/biome_service/src/file_handlers/css.rs b/crates/biome_service/src/file_handlers/css.rs index 54ebae9dc92b..703ecd5bc550 100644 --- a/crates/biome_service/src/file_handlers/css.rs +++ b/crates/biome_service/src/file_handlers/css.rs @@ -156,6 +156,8 @@ impl ServiceLanguage for CssLanguage { globals: Vec::new(), preferred_quote, jsx_runtime: None, + jsx_factory: None, + jsx_fragment_factory: None, }; AnalyzerOptions { diff --git a/crates/biome_service/src/file_handlers/javascript.rs b/crates/biome_service/src/file_handlers/javascript.rs index d093b063d301..5df09441453e 100644 --- a/crates/biome_service/src/file_handlers/javascript.rs +++ b/crates/biome_service/src/file_handlers/javascript.rs @@ -24,7 +24,7 @@ use biome_analyze::{ AnalysisFilter, AnalyzerConfiguration, AnalyzerOptions, ControlFlow, Never, QueryMatch, RuleCategoriesBuilder, RuleCategory, RuleError, RuleFilter, }; -use biome_configuration::javascript::JsxRuntime; +use biome_configuration::javascript::{JsxFactory, JsxRuntime}; use biome_diagnostics::{category, Applicability, Diagnostic, DiagnosticExt, Severity}; use biome_formatter::{ AttributePosition, BracketSpacing, FormatError, IndentStyle, IndentWidth, LineEnding, @@ -89,11 +89,23 @@ pub struct JsOrganizeImportsSettings {} #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct JsEnvironmentSettings { pub jsx_runtime: JsxRuntime, + pub jsx_factory: Option, + pub jsx_fragment_factory: Option, } -impl From for JsEnvironmentSettings { - fn from(jsx_runtime: JsxRuntime) -> Self { - Self { jsx_runtime } +impl From<(JsxRuntime, Option, Option)> for JsEnvironmentSettings { + fn from( + (jsx_runtime, jsx_factory, jsx_fragment_factory): ( + JsxRuntime, + Option, + Option, + ), + ) -> Self { + Self { + jsx_runtime, + jsx_factory, + jsx_fragment_factory, + } } } @@ -209,6 +221,8 @@ impl ServiceLanguage for JsLanguage { .unwrap_or_default(); let mut jsx_runtime = None; + let mut jsx_factory = None; + let mut jsx_fragment_factory = None; let mut globals = Vec::new(); if let (Some(overrides), Some(global)) = (overrides, global) { @@ -223,6 +237,21 @@ impl ServiceLanguage for JsLanguage { }, ); + jsx_factory = overrides.override_jsx_factory( + path, + global.languages.javascript.environment.jsx_factory.as_ref(), + ); + + jsx_fragment_factory = overrides.override_jsx_fragment_factory( + path, + global + .languages + .javascript + .environment + .jsx_fragment_factory + .as_ref(), + ); + globals.extend( overrides .override_js_globals(path, &global.languages.javascript.globals) @@ -274,6 +303,8 @@ impl ServiceLanguage for JsLanguage { globals, preferred_quote, jsx_runtime, + jsx_factory: jsx_factory.map(|f| f.into_string().into()), + jsx_fragment_factory: jsx_fragment_factory.map(|f| f.into_string().into()), }; AnalyzerOptions { diff --git a/crates/biome_service/src/file_handlers/json.rs b/crates/biome_service/src/file_handlers/json.rs index 6d98907cdb22..78c676a30287 100644 --- a/crates/biome_service/src/file_handlers/json.rs +++ b/crates/biome_service/src/file_handlers/json.rs @@ -140,6 +140,8 @@ impl ServiceLanguage for JsonLanguage { globals: vec![], preferred_quote: PreferredQuote::Double, jsx_runtime: Default::default(), + jsx_factory: None, + jsx_fragment_factory: None, }; AnalyzerOptions { configuration, diff --git a/crates/biome_service/src/file_handlers/mod.rs b/crates/biome_service/src/file_handlers/mod.rs index da1f1adbbf95..5cae296d94a1 100644 --- a/crates/biome_service/src/file_handlers/mod.rs +++ b/crates/biome_service/src/file_handlers/mod.rs @@ -37,7 +37,7 @@ use biome_parser::AnyParse; use biome_project::PackageJson; use biome_rowan::{FileSourceError, NodeCache}; use biome_string_case::StrExtension; -pub use javascript::JsFormatterSettings; +pub use javascript::{JsEnvironmentSettings, JsFormatterSettings}; use std::borrow::Cow; use std::ffi::OsStr; use std::path::Path; diff --git a/crates/biome_service/src/settings.rs b/crates/biome_service/src/settings.rs index 7b3fb1cd0fa1..c61dd2e91999 100644 --- a/crates/biome_service/src/settings.rs +++ b/crates/biome_service/src/settings.rs @@ -3,7 +3,7 @@ use crate::{Matcher, WorkspaceError}; use biome_analyze::{AnalyzerOptions, AnalyzerRules}; use biome_configuration::analyzer::assists::AssistsConfiguration; use biome_configuration::diagnostics::InvalidIgnorePattern; -use biome_configuration::javascript::JsxRuntime; +use biome_configuration::javascript::{JsxFactory, JsxRuntime}; use biome_configuration::organize_imports::OrganizeImports; use biome_configuration::{ push_to_analyzer_rules, BiomeDiagnostic, FilesConfiguration, FormatterConfiguration, @@ -564,6 +564,8 @@ pub struct LanguageListSettings { impl From for LanguageSettings { fn from(javascript: JavascriptConfiguration) -> Self { + use crate::file_handlers::JsEnvironmentSettings; + let mut language_setting: LanguageSettings = LanguageSettings::default(); let formatter = javascript.formatter; @@ -584,7 +586,11 @@ impl From for LanguageSettings { javascript.parser.unsafe_parameter_decorators_enabled; language_setting.globals = Some(javascript.globals.into_index_set()); - language_setting.environment = javascript.jsx_runtime.into(); + language_setting.environment = JsEnvironmentSettings { + jsx_runtime: javascript.jsx_runtime, + jsx_factory: Some(javascript.jsx_factory), + jsx_fragment_factory: Some(javascript.jsx_fragment_factory), + }; language_setting.linter.enabled = Some(javascript.linter.enabled); language_setting @@ -957,6 +963,49 @@ impl OverrideSettings { .unwrap_or(base_setting) } + pub fn override_jsx_factory( + &self, + path: &BiomePath, + base_setting: Option<&JsxFactory>, + ) -> Option { + self.patterns + .iter() + // Reverse the traversal as only the last override takes effect + .rev() + .find_map(|pattern| { + if pattern.include.matches_path(path) && !pattern.exclude.matches_path(path) { + pattern.languages.javascript.environment.jsx_factory.clone() + } else { + None + } + }) + .or_else(|| base_setting.cloned()) + } + + pub fn override_jsx_fragment_factory( + &self, + path: &BiomePath, + base_setting: Option<&JsxFactory>, + ) -> Option { + self.patterns + .iter() + // Reverse the traversal as only the last override takes effect + .rev() + .find_map(|pattern| { + if pattern.include.matches_path(path) && !pattern.exclude.matches_path(path) { + pattern + .languages + .javascript + .environment + .jsx_fragment_factory + .clone() + } else { + None + } + }) + .or_else(|| base_setting.cloned()) + } + /// It scans the current override rules and return the json format that of the first override is matched pub fn to_override_json_format_options( &self, diff --git a/crates/biome_test_utils/src/lib.rs b/crates/biome_test_utils/src/lib.rs index 8906f0cf7e06..442ed206db85 100644 --- a/crates/biome_test_utils/src/lib.rs +++ b/crates/biome_test_utils/src/lib.rs @@ -43,6 +43,8 @@ pub fn create_analyzer_options( globals: vec![], preferred_quote: PreferredQuote::Double, jsx_runtime: Some(JsxRuntime::Transparent), + jsx_factory: None, + jsx_fragment_factory: None, }; let options_file = input_file.with_extension("options.json"); if let Ok(json) = std::fs::read_to_string(options_file.clone()) { @@ -93,6 +95,16 @@ pub fn create_analyzer_options( ReactClassic => Some(JsxRuntime::ReactClassic), Transparent => Some(JsxRuntime::Transparent), }; + analyzer_configuration.jsx_factory = configuration + .javascript + .as_ref() + .and_then(|js| js.jsx_factory.clone().map(|f| f.into_string().into())); + analyzer_configuration.jsx_fragment_factory = + configuration.javascript.as_ref().and_then(|js| { + js.jsx_fragment_factory + .clone() + .map(|f| f.into_string().into()) + }); analyzer_configuration.globals = configuration .javascript .as_ref() diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 7a5f8934758e..23c053fd8b17 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -246,6 +246,14 @@ export interface PartialJavascriptConfiguration { If defined here, they should not emit diagnostics. */ globals?: StringSet; + /** + * Indicates the name of the factory function used to create JSX elements. + */ + jsxFactory?: JsxFactory; + /** + * Indicates the name of the factory function used to create JSX fragments. + */ + jsxFragmentFactory?: JsxFactory; /** * Indicates the type of runtime or transformation used for interpreting JSX. */ @@ -535,6 +543,7 @@ export interface PartialJavascriptFormatter { */ trailingCommas?: TrailingCommas; } +export type JsxFactory = string; /** * Indicates the type of runtime or transformation used for interpreting JSX. */ diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 2e096a5ac1b0..f1c294353dc2 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -1405,6 +1405,14 @@ "description": "A list of global bindings that should be ignored by the analyzers\n\nIf defined here, they should not emit diagnostics.", "anyOf": [{ "$ref": "#/definitions/StringSet" }, { "type": "null" }] }, + "jsxFactory": { + "description": "Indicates the name of the factory function used to create JSX elements.", + "anyOf": [{ "$ref": "#/definitions/JsxFactory" }, { "type": "null" }] + }, + "jsxFragmentFactory": { + "description": "Indicates the name of the factory function used to create JSX fragments.", + "anyOf": [{ "$ref": "#/definitions/JsxFactory" }, { "type": "null" }] + }, "jsxRuntime": { "description": "Indicates the type of runtime or transformation used for interpreting JSX.", "anyOf": [{ "$ref": "#/definitions/JsxRuntime" }, { "type": "null" }] @@ -1647,6 +1655,7 @@ }, "additionalProperties": false }, + "JsxFactory": { "type": "string" }, "JsxRuntime": { "description": "Indicates the type of runtime or transformation used for interpreting JSX.", "oneOf": [