Skip to content

Commit 676ceb4

Browse files
committed
feat(tailwind): plumbing for tailwind analyze crate
1 parent 4cafb71 commit 676ceb4

File tree

38 files changed

+2754
-593
lines changed

38 files changed

+2754
-593
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ biome_ruledoc_utils = { path = "./crates/biome_ruledoc_utils" }
9797
biome_service = { path = "./crates/biome_service" }
9898
biome_string_case = { path = "./crates/biome_string_case", version = "0.5.7", features = ["biome_rowan"] }
9999
biome_suppression = { path = "./crates/biome_suppression", version = "0.5.7" }
100+
biome_tailwind_analyze = { path = "./crates/biome_tailwind_analyze", version = "0.0.1" }
100101
biome_tailwind_factory = { path = "./crates/biome_tailwind_factory", version = "0.0.1" }
101102
biome_tailwind_parser = { path = "./crates/biome_tailwind_parser", version = "0.0.1" }
102103
biome_tailwind_syntax = { path = "./crates/biome_tailwind_syntax", version = "0.0.1" }

crates/biome_analyze_macros/src/group_macro.rs

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ fn generate_group_code(category: &str, group: &str) -> Result<TokenStream> {
6868
// Use CARGO_MANIFEST_DIR to get the base path since proc_macro::Span::source_file
6969
// is not available on stable Rust
7070
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").context("CARGO_MANIFEST_DIR not set")?;
71-
let base_path = Utf8PathBuf::from(manifest_dir).join("src").join(category);
71+
let manifest_path = Utf8PathBuf::from(&manifest_dir);
72+
let base_path = manifest_path.join("src").join(category);
7273

7374
// Discover all rules in the group directory
7475
let group_dir = base_path.join(group);
@@ -116,11 +117,37 @@ fn generate_group_code(category: &str, group: &str) -> Result<TokenStream> {
116117
);
117118
}
118119

120+
let group_name = format_ident!("{}", Case::Pascal.convert(group));
121+
119122
if rules.is_empty() {
120-
bail!("No rules found in directory: {}", group_dir);
121-
}
123+
// fallback, if there are no rules, still generate the group anyway to avoid breaking the build
124+
let crate_name = manifest_path
125+
.file_name()
126+
.context("Failed to determine analyzer crate name")?;
127+
let language = analyzer_language(crate_name)?;
122128

123-
let group_name = format_ident!("{}", Case::Pascal.convert(group));
129+
let tokens = quote! {
130+
pub enum #group_name {}
131+
132+
impl biome_analyze::RuleGroup for #group_name {
133+
type Language = #language;
134+
type Category = super::Category;
135+
136+
const NAME: &'static str = #group;
137+
138+
fn record_rules<V: biome_analyze::RegistryVisitor<Self::Language> + ?Sized>(_: &mut V) {}
139+
}
140+
141+
pub(self) use #group_name as Group;
142+
};
143+
144+
let formatted = format_code(tokens)?;
145+
let token_stream = formatted
146+
.parse::<proc_macro2::TokenStream>()
147+
.map_err(|e| anyhow::anyhow!("Failed to parse formatted code as TokenStream: {}", e))?;
148+
149+
return Ok(token_stream.into());
150+
}
124151

125152
let (rule_imports, rule_names): (Vec<_>, Vec<_>) = rules.into_values().unzip();
126153

@@ -174,6 +201,18 @@ fn generate_group_code(category: &str, group: &str) -> Result<TokenStream> {
174201
Ok(token_stream.into())
175202
}
176203

204+
fn analyzer_language(crate_name: &str) -> Result<proc_macro2::TokenStream> {
205+
Ok(match crate_name {
206+
"biome_css_analyze" => quote!(biome_css_syntax::CssLanguage),
207+
"biome_graphql_analyze" => quote!(biome_graphql_syntax::GraphqlLanguage),
208+
"biome_html_analyze" => quote!(biome_html_syntax::HtmlLanguage),
209+
"biome_js_analyze" => quote!(biome_js_syntax::JsLanguage),
210+
"biome_json_analyze" => quote!(biome_json_syntax::JsonLanguage),
211+
"biome_tailwind_analyze" => quote!(biome_tailwind_syntax::TailwindLanguage),
212+
_ => bail!("Unsupported analyzer crate: {}", crate_name),
213+
})
214+
}
215+
177216
fn format_code(tokens: proc_macro2::TokenStream) -> Result<String> {
178217
// Parse the token stream into a syn file
179218
let file = syn::parse2::<syn::File>(tokens).context("Failed to parse tokens as syn::File")?;

crates/biome_configuration_macros/Cargo.toml

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,23 @@ categories.workspace = true
1313
proc-macro = true
1414

1515
[dependencies]
16-
biome_analyze = { workspace = true }
17-
biome_css_analyze = { workspace = true }
18-
biome_css_syntax = { workspace = true }
19-
biome_graphql_analyze = { workspace = true }
20-
biome_graphql_syntax = { workspace = true }
21-
biome_html_analyze = { workspace = true }
22-
biome_html_syntax = { workspace = true }
23-
biome_js_analyze = { workspace = true }
24-
biome_js_syntax = { workspace = true }
25-
biome_json_analyze = { workspace = true }
26-
biome_json_syntax = { workspace = true }
27-
biome_string_case = { workspace = true }
28-
proc-macro2 = { workspace = true }
29-
pulldown-cmark = { version = "0.13.0" }
30-
quote = { workspace = true }
16+
biome_analyze = { workspace = true }
17+
biome_css_analyze = { workspace = true }
18+
biome_css_syntax = { workspace = true }
19+
biome_graphql_analyze = { workspace = true }
20+
biome_graphql_syntax = { workspace = true }
21+
biome_html_analyze = { workspace = true }
22+
biome_html_syntax = { workspace = true }
23+
biome_js_analyze = { workspace = true }
24+
biome_js_syntax = { workspace = true }
25+
biome_json_analyze = { workspace = true }
26+
biome_json_syntax = { workspace = true }
27+
biome_string_case = { workspace = true }
28+
biome_tailwind_analyze = { workspace = true }
29+
biome_tailwind_syntax = { workspace = true }
30+
proc-macro2 = { workspace = true }
31+
pulldown-cmark = { version = "0.13.0" }
32+
quote = { workspace = true }
3133

3234
[lints]
3335
workspace = true

crates/biome_configuration_macros/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ fn collect_lint_rules() -> LintRulesVisitor {
4646
biome_css_analyze::visit_registry(&mut lint_visitor);
4747
biome_graphql_analyze::visit_registry(&mut lint_visitor);
4848
biome_html_analyze::visit_registry(&mut lint_visitor);
49+
biome_tailwind_analyze::visit_registry(&mut lint_visitor);
4950

5051
lint_visitor
5152
}

crates/biome_configuration_macros/src/visitors.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use biome_graphql_syntax::GraphqlLanguage;
88
use biome_html_syntax::HtmlLanguage;
99
use biome_js_syntax::JsLanguage;
1010
use biome_json_syntax::JsonLanguage;
11+
use biome_tailwind_syntax::TailwindLanguage;
1112

1213
// ======= LINT ======
1314
#[derive(Default)]
@@ -119,6 +120,25 @@ impl RegistryVisitor<HtmlLanguage> for LintRulesVisitor {
119120
}
120121
}
121122

123+
impl RegistryVisitor<TailwindLanguage> for LintRulesVisitor {
124+
fn record_category<C: GroupCategory<Language = TailwindLanguage>>(&mut self) {
125+
if matches!(C::CATEGORY, RuleCategory::Lint) {
126+
C::record_groups(self);
127+
}
128+
}
129+
130+
fn record_rule<R>(&mut self)
131+
where
132+
R: Rule<Options: Default, Query: Queryable<Language = TailwindLanguage, Output: Clone>>
133+
+ 'static,
134+
{
135+
self.groups
136+
.entry(<R::Group as RuleGroup>::NAME)
137+
.or_default()
138+
.insert(R::METADATA.name, R::METADATA);
139+
}
140+
}
141+
122142
// ======= ASSIST ======
123143
#[derive(Default)]
124144
pub struct AssistActionsVisitor {

crates/biome_service/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ biome_project_layout = { workspace = true }
6363
biome_resolver = { workspace = true }
6464
biome_rowan = { workspace = true, features = ["serde"] }
6565
biome_string_case = { workspace = true }
66+
biome_tailwind_analyze = { workspace = true }
67+
biome_tailwind_parser = { workspace = true }
68+
biome_tailwind_syntax = { workspace = true }
6669
biome_text_edit = { workspace = true }
6770
boxcar = { workspace = true }
6871
bpaf = { workspace = true }

crates/biome_service/src/embed/detector.rs

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
use crate::embed::types::{EmbedCandidate, GuestLanguage, TemplateTagKind};
2-
use crate::workspace::DocumentFileSource;
1+
use crate::embed::types::{EmbedCandidate, EmbedDetectionContext, GuestLanguage, TemplateTagKind};
32

43
/// A single embed detector. Entirely const-constructible.
54
///
@@ -38,6 +37,12 @@ pub(crate) enum EmbedDetector {
3837
/// Matches `EmbedCandidate::Directive`. Always matches (no pattern).
3938
/// The guest language depends on the host framework.
4039
Directive { target: EmbedTarget },
40+
41+
/// Matches `EmbedCandidate::AttributeValue` by attribute name.
42+
AttributeValue { target: EmbedTarget },
43+
44+
/// Matches `EmbedCandidate::CallArgument` by callee name.
45+
CallArgument { target: EmbedTarget },
4146
}
4247

4348
impl EmbedDetector {
@@ -47,21 +52,21 @@ impl EmbedDetector {
4752
pub fn try_match(
4853
&self,
4954
candidate: &EmbedCandidate,
50-
file_source: &DocumentFileSource,
55+
context: &EmbedDetectionContext,
5156
) -> Option<GuestLanguage> {
5257
match (self, candidate) {
5358
// Element detector VS an Element candidate: match by tag name
5459
(Self::Element { tag, target }, EmbedCandidate::Element { tag_name, .. }) => {
5560
if tag_name.text().eq_ignore_ascii_case(tag) {
56-
target.resolve(candidate, file_source)
61+
target.resolve(candidate, context)
5762
} else {
5863
None
5964
}
6065
}
6166

6267
// Frontmatter detector + Frontmatter candidate: always matches
6368
(Self::Frontmatter { target }, EmbedCandidate::Frontmatter { .. }) => {
64-
target.resolve(candidate, file_source)
69+
target.resolve(candidate, context)
6570
}
6671

6772
// TemplateTag detector + TaggedTemplate candidate with Identifier tag
@@ -73,7 +78,7 @@ impl EmbedDetector {
7378
},
7479
) => {
7580
if name.text() == *tag {
76-
target.resolve(candidate, file_source)
81+
target.resolve(candidate, context)
7782
} else {
7883
None
7984
}
@@ -87,14 +92,14 @@ impl EmbedDetector {
8792
) => match tag {
8893
TemplateTagKind::MemberExpression { object: obj, .. } => {
8994
if obj.text() == *object {
90-
target.resolve(candidate, file_source)
95+
target.resolve(candidate, context)
9196
} else {
9297
None
9398
}
9499
}
95100
TemplateTagKind::CallExpression { callee } => {
96101
if callee.text() == *object {
97-
target.resolve(candidate, file_source)
102+
target.resolve(candidate, context)
98103
} else {
99104
None
100105
}
@@ -104,12 +109,20 @@ impl EmbedDetector {
104109

105110
// TextExpression detector + TextExpression candidate: always matches
106111
(Self::TextExpression { target }, EmbedCandidate::TextExpression { .. }) => {
107-
target.resolve(candidate, file_source)
112+
target.resolve(candidate, context)
108113
}
109114

110115
// Directive detector + Directive candidate: always matches
111116
(Self::Directive { target }, EmbedCandidate::Directive { .. }) => {
112-
target.resolve(candidate, file_source)
117+
target.resolve(candidate, context)
118+
}
119+
120+
(Self::AttributeValue { target }, EmbedCandidate::AttributeValue { .. }) => {
121+
target.resolve(candidate, context)
122+
}
123+
124+
(Self::CallArgument { target }, EmbedCandidate::CallArgument { .. }) => {
125+
target.resolve(candidate, context)
113126
}
114127

115128
// Mismatched variant — no match
@@ -126,7 +139,7 @@ pub(crate) enum EmbedTarget {
126139
/// Guest language depends on element attributes / host file source.
127140
Dynamic {
128141
/// Function that is used to determine the guest language.
129-
resolver: fn(&EmbedCandidate, &DocumentFileSource) -> Option<GuestLanguage>,
142+
resolver: fn(&EmbedCandidate, &EmbedDetectionContext) -> Option<GuestLanguage>,
130143
/// Possible fallback. Use `None` to tell the matcher to ignore the snippet
131144
fallback: Option<GuestLanguage>,
132145
},
@@ -137,11 +150,11 @@ impl EmbedTarget {
137150
fn resolve(
138151
&self,
139152
candidate: &EmbedCandidate,
140-
file_source: &DocumentFileSource,
153+
context: &EmbedDetectionContext,
141154
) -> Option<GuestLanguage> {
142155
match self {
143156
Self::Static(g) => Some(*g),
144-
Self::Dynamic { resolver, fallback } => resolver(candidate, file_source).or(*fallback),
157+
Self::Dynamic { resolver, fallback } => resolver(candidate, context).or(*fallback),
145158
}
146159
}
147160
}

0 commit comments

Comments
 (0)