Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(js_semantic): support jsxFactory and jsxFragmentFactory #3761

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions crates/biome_analyze/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ where
options: &'a R::Options,
preferred_quote: &'a PreferredQuote,
jsx_runtime: Option<JsxRuntime>,
jsx_factory: Option<&'a str>,
jsx_fragment_factory: Option<&'a str>,
}

impl<'a, R> RuleContext<'a, R>
Expand All @@ -37,6 +39,8 @@ where
options: &'a R::Options,
preferred_quote: &'a PreferredQuote,
jsx_runtime: Option<JsxRuntime>,
jsx_factory: Option<&'a str>,
jsx_fragment_factory: Option<&'a str>,
) -> Result<Self, Error> {
let rule_key = RuleKey::rule::<R>();
Ok(Self {
Expand All @@ -49,6 +53,8 @@ where
options,
preferred_quote,
jsx_runtime,
jsx_factory,
jsx_fragment_factory,
})
}

Expand Down Expand Up @@ -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)
Expand Down
46 changes: 46 additions & 0 deletions crates/biome_analyze/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -51,6 +52,33 @@ impl AnalyzerRules {
}
}

/// Jsx factory namespace
#[derive(Debug)]
pub struct JsxFactory(Box<str>);

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<String> 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 {
Expand All @@ -67,6 +95,16 @@ pub struct AnalyzerConfiguration {

/// Indicates the type of runtime or transformation used for interpreting JSX.
pub jsx_runtime: Option<JsxRuntime>,

/// 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<JsxFactory>,

/// 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<JsxFactory>,
}

/// A set of information useful to the analyzer infrastructure
Expand All @@ -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<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 @@ -400,6 +400,8 @@ impl<L: Language + Default> RegistryRule<L> {
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::<R>().unwrap_or_default();
let ctx = match RuleContext::new(
&query_result,
Expand All @@ -410,6 +412,8 @@ impl<L: Language + Default> RegistryRule<L> {
&options,
preferred_quote,
jsx_runtime,
jsx_factory,
jsx_fragment_factory,
) {
Ok(ctx) => ctx,
Err(error) => return Err(error),
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 @@ -357,6 +357,8 @@ where
&options,
preferred_quote,
self.options.jsx_runtime(),
self.options.jsx_factory(),
self.options.jsx_fragment_factory(),
)
.ok()?;

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions crates/biome_configuration/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
106 changes: 105 additions & 1 deletion crates/biome_configuration/src/javascript/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -100,6 +117,93 @@ impl FromStr for JsxRuntime {
}
}

fn deserialize_optional_jsx_factory_from_string<'de, D>(
deserializer: D,
) -> Result<Option<JsxFactory>, 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<JsxFactory> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if this is the right place to put, thinking of this is a quite general utility.

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())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JsxFactory only stores its namespace. We eagerly parse the expression to support configuration validation.
Using React instead of React.createElement or React.Fragment to make it more performant.

}
}

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<biome_deserialize::DeserializationDiagnostic>,
) -> Option<Self> {
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 "<Emphasis>{"identifier"}</Emphasis>" or "<Emphasis>{"qualified name"}</Emphasis>", but received "<Emphasis>{format_args!("{}", factory.text())}</Emphasis>"."
),
).with_range(value.range()));
None
})
}
}

impl FromStr for JsxFactory {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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))]
Expand Down
Loading
Loading