Skip to content

feat(lint): add nursery rule noTailwindArbitraryValue#10094

Open
THEjacob1000 wants to merge 19 commits into
biomejs:mainfrom
THEjacob1000:feat/no-arbitrary-value
Open

feat(lint): add nursery rule noTailwindArbitraryValue#10094
THEjacob1000 wants to merge 19 commits into
biomejs:mainfrom
THEjacob1000:feat/no-arbitrary-value

Conversation

@THEjacob1000
Copy link
Copy Markdown

@THEjacob1000 THEjacob1000 commented Apr 23, 2026

Summary

Adds the nursery lint rule noTailwindArbitraryValue for Tailwind CSS class strings in JSX and HTML.

The rule reports Tailwind arbitrary values such as w-[400px], text-[#555], and [color:red]. It splits class strings on whitespace, parses each candidate with Biome's Tailwind parser, and reports arbitrary candidates plus arbitrary functional values/modifiers while allowing arbitrary variants like [&:nth-child(3)]:px-2.

Related Issue: #6502

AI Usage

This PR was implemented with assistance from Codex AI assistant.

Test Plan

  • Snapshot tests in crates/biome_js_analyze/tests/specs/nursery/noTailwindArbitraryValue/
  • Snapshot tests in crates/biome_html_analyze/tests/specs/nursery/noTailwindArbitraryValue/
  • cargo run -p rules_check
  • cargo test -p biome_js_analyze no_tailwind_arbitrary_value --test spec_tests
  • cargo test -p biome_html_analyze no_tailwind_arbitrary_value --test spec_tests
  • just l

Docs

Rule documentation is inline via rustdoc on the JSX and HTML rule implementations, including invalid/valid examples and options.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 23, 2026

🦋 Changeset detected

Latest commit: 39fea32

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
Name Type
@biomejs/biome Patch
@biomejs/cli-win32-x64 Patch
@biomejs/cli-win32-arm64 Patch
@biomejs/cli-darwin-x64 Patch
@biomejs/cli-darwin-arm64 Patch
@biomejs/cli-linux-x64 Patch
@biomejs/cli-linux-arm64 Patch
@biomejs/cli-linux-x64-musl Patch
@biomejs/cli-linux-arm64-musl Patch
@biomejs/wasm-web Patch
@biomejs/wasm-bundler Patch
@biomejs/wasm-nodejs Patch
@biomejs/backend-jsonrpc Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added A-Project Area: project A-Linter Area: linter L-JavaScript Language: JavaScript and super languages A-Diagnostic Area: diagnostocis labels Apr 23, 2026
@dyc3 dyc3 added the M-Likely Agent Meta: this was likely an automated PR without a human in the loop label Apr 23, 2026
@THEjacob1000 THEjacob1000 changed the title feat(lint): add nursery rule noArbitraryValue 🤖🤖🤖 feat(lint): add nursery rule noArbitraryValue Apr 27, 2026
@THEjacob1000 THEjacob1000 force-pushed the feat/no-arbitrary-value branch 3 times, most recently from 86571c0 to c14c596 Compare April 27, 2026 04:00
@THEjacob1000 THEjacob1000 force-pushed the feat/no-arbitrary-value branch from c14c596 to 1bde577 Compare April 27, 2026 04:01
@THEjacob1000 THEjacob1000 marked this pull request as ready for review April 27, 2026 04:01
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a new nursery lint rule noTailwindArbitraryValue for HTML and JSX that detects Tailwind arbitrary-value syntax (e.g. w-[400px]) in class-like strings, configured utility functions and tagged templates. Introduces parsing helpers in a new lint_utils module (exported from the tailwind parser crate), a new rule options type, test fixtures (valid/invalid) for HTML/JSX and functions, and workspace Cargo dependency entries for Tailwind parser/syntax. Also adjusts HTML attribute handling to be case-insensitive in several existing checks.

Suggested labels

A-Parser

Suggested reviewers

  • dyc3
  • ematipico
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description thoroughly documents the rule's purpose, functionality, related issue, testing approach, and includes AI usage disclosure—all directly relevant to the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title 'feat(lint): add nursery rule noTailwindArbitraryValue' directly and clearly describes the main change—adding a new nursery lint rule for detecting Tailwind arbitrary values.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (5)
crates/biome_js_analyze/tests/specs/nursery/noArbitraryValue/invalid_functions.jsx (1)

1-7: Solid coverage of the function/template surface.

Strings, multi-arg, object keys, plain tagged template, and static-member tagged template are all here — that's exactly the matrix the rule's matcher cares about. Optional nit: an array-literal arg like clsx(["w-[400px]"]) would round out the call-expression coverage if the rule supports it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@crates/biome_js_analyze/tests/specs/nursery/noArbitraryValue/invalid_functions.jsx`
around lines 1 - 7, Add an extra test case that supplies an array-literal
argument to the call-expression surface the rule matches (to cover the optional
nit mentioned); specifically add a call such as using the existing
functions/identifiers clsx, cn or classnames with an array argument (e.g.,
clsx([...]) or classnames([...])) so the test file that already includes clsx,
cn, classnames, tw and tw.div also covers array-literal argument forms for the
rule's matcher.
.changeset/add-no-arbitrary-value.md (1)

1-6: Changeset reads well.

patch + main is the correct combo for a new nursery rule, and the rule link is in place. Tiny optional polish: you could mention the default class/className attribute coverage alongside the "configured utility functions and tagged templates" — that's the headline behaviour users will hit out of the box.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.changeset/add-no-arbitrary-value.md around lines 1 - 6, Update the
changeset text for the new nursery rule `noArbitraryValue` in
.changeset/add-no-arbitrary-value.md to explicitly mention that, in addition to
configured utility functions and tagged templates, the rule covers the default
`class`/`className` attributes out of the box; edit the descriptive paragraph to
append a short phrase like "and the default class/className attributes" so users
immediately see the primary, default behaviour of `noArbitraryValue`.
crates/biome_js_analyze/src/lint/nursery/no_arbitrary_value.rs (2)

67-67: Drop fix_kind: FixKind::None — the rule doesn't implement action().

This rule only emits diagnostics, so the field should be omitted entirely (and FixKind removed from the import on line 2). Specifying FixKind::None falsely advertises a fix path that isn't implemented.

♻️ Proposed diff
-use biome_analyze::{Ast, FixKind, Rule, RuleDiagnostic, context::RuleContext, declare_lint_rule};
+use biome_analyze::{Ast, Rule, RuleDiagnostic, context::RuleContext, declare_lint_rule};
@@
     pub NoArbitraryValue {
         version: "next",
         name: "noArbitraryValue",
         language: "jsx",
         recommended: false,
-        fix_kind: FixKind::None,
     }

Based on learnings: "When defining lint rules (declare_lint_rule!), only specify fix_kind if the rule implements an action(...) function. Rules that only emit diagnostics without a code fix should omit fix_kind."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_analyze/src/lint/nursery/no_arbitrary_value.rs` at line 67,
Remove the unnecessary fix_kind declaration and import: in the
declare_lint_rule! invocation (the rule struct defined in no_arbitrary_value.rs)
delete the `fix_kind: FixKind::None,` entry and remove `FixKind` from the
imports at the top, since this rule does not implement an `action()`/fix path;
leave the rule as diagnostics-only so it no longer advertises a nonexistent fix.

80-87: Minor: round-tripping options into UseSortedClassesOptions is awkward.

You're cloning both Box<[Box<str>]> allocations purely to satisfy should_visit's signature. If you adopt the refactor suggested in no_arbitrary_value.rs options (newtype/shared inner), this could just be node.should_visit(&options.0) or similar without the clone. Pairs with the duplication note in the rule_options file — feel free to defer to a follow-up.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_analyze/src/lint/nursery/no_arbitrary_value.rs` around lines
80 - 87, The current code creates a temporary UseSortedClassesOptions by cloning
Box<[Box<str>]> fields to satisfy node.should_visit, which is inefficient;
update the options types so you can pass a reference instead of cloning: either
change UseSortedClassesOptions/should_visit to accept a reference to the shared
inner options or introduce a newtype/shared inner for the existing options
(e.g., options.0) and call node.should_visit(&options.0) directly; remove the
clones and the intermediate UseSortedClassesOptions construction in the block
around UseSortedClassesOptions and node.should_visit to avoid unnecessary
allocations.
crates/biome_rule_options/src/no_arbitrary_value.rs (1)

1-88: Heads-up: this is a near-verbatim clone of UseSortedClassesOptions.

Per crates/biome_rule_options/src/use_sorted_classes.rs:1-88, the struct shape, Merge impl, ALLOWED_OPTIONS, and DeserializationVisitor are identical apart from doc strings and the type name. Since the rule itself converts back into UseSortedClassesOptions to call should_visit, consider either:

  • factoring the visitor/Merge/ALLOWED_OPTIONS into a shared helper that both option types reuse, or
  • having NoArbitraryValueOptions wrap UseSortedClassesOptions (e.g. #[serde(transparent)] newtype or a pub inner: UseSortedClassesOptions field) so the two stay in lockstep automatically.

Not a blocker for this PR — the duplication just means future tweaks to attributes/functions deserialization must be applied in two places.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_rule_options/src/no_arbitrary_value.rs` around lines 1 - 88, The
NoArbitraryValueOptions code duplicates UseSortedClassesOptions behavior; fix by
either extracting the shared pieces (ALLOWED_OPTIONS, Merge impl, and
DeserializationVisitor) into a common helper/utility that both
NoArbitraryValueOptions and UseSortedClassesOptions reuse, or make
NoArbitraryValueOptions a thin wrapper around UseSortedClassesOptions (e.g. add
a pub inner: UseSortedClassesOptions field or #[serde(transparent)] newtype) and
update NoArbitraryValueOptions::merge_with, Deserializable impl, and its
DeserializationVisitor to delegate to/use UseSortedClassesOptions’ Merge and
visitor logic so attributes/functions deserialization stays in sync.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In @.changeset/add-no-arbitrary-value.md:
- Around line 1-6: Update the changeset text for the new nursery rule
`noArbitraryValue` in .changeset/add-no-arbitrary-value.md to explicitly mention
that, in addition to configured utility functions and tagged templates, the rule
covers the default `class`/`className` attributes out of the box; edit the
descriptive paragraph to append a short phrase like "and the default
class/className attributes" so users immediately see the primary, default
behaviour of `noArbitraryValue`.

In `@crates/biome_js_analyze/src/lint/nursery/no_arbitrary_value.rs`:
- Line 67: Remove the unnecessary fix_kind declaration and import: in the
declare_lint_rule! invocation (the rule struct defined in no_arbitrary_value.rs)
delete the `fix_kind: FixKind::None,` entry and remove `FixKind` from the
imports at the top, since this rule does not implement an `action()`/fix path;
leave the rule as diagnostics-only so it no longer advertises a nonexistent fix.
- Around line 80-87: The current code creates a temporary
UseSortedClassesOptions by cloning Box<[Box<str>]> fields to satisfy
node.should_visit, which is inefficient; update the options types so you can
pass a reference instead of cloning: either change
UseSortedClassesOptions/should_visit to accept a reference to the shared inner
options or introduce a newtype/shared inner for the existing options (e.g.,
options.0) and call node.should_visit(&options.0) directly; remove the clones
and the intermediate UseSortedClassesOptions construction in the block around
UseSortedClassesOptions and node.should_visit to avoid unnecessary allocations.

In
`@crates/biome_js_analyze/tests/specs/nursery/noArbitraryValue/invalid_functions.jsx`:
- Around line 1-7: Add an extra test case that supplies an array-literal
argument to the call-expression surface the rule matches (to cover the optional
nit mentioned); specifically add a call such as using the existing
functions/identifiers clsx, cn or classnames with an array argument (e.g.,
clsx([...]) or classnames([...])) so the test file that already includes clsx,
cn, classnames, tw and tw.div also covers array-literal argument forms for the
rule's matcher.

In `@crates/biome_rule_options/src/no_arbitrary_value.rs`:
- Around line 1-88: The NoArbitraryValueOptions code duplicates
UseSortedClassesOptions behavior; fix by either extracting the shared pieces
(ALLOWED_OPTIONS, Merge impl, and DeserializationVisitor) into a common
helper/utility that both NoArbitraryValueOptions and UseSortedClassesOptions
reuse, or make NoArbitraryValueOptions a thin wrapper around
UseSortedClassesOptions (e.g. add a pub inner: UseSortedClassesOptions field or
#[serde(transparent)] newtype) and update NoArbitraryValueOptions::merge_with,
Deserializable impl, and its DeserializationVisitor to delegate to/use
UseSortedClassesOptions’ Merge and visitor logic so attributes/functions
deserialization stays in sync.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e3d13417-00e7-4920-a651-efae4c78d54b

📥 Commits

Reviewing files that changed from the base of the PR and between e718a7f and 1bde577.

⛔ Files ignored due to path filters (10)
  • Cargo.lock is excluded by !**/*.lock and included by **
  • crates/biome_configuration/src/analyzer/linter/rules.rs is excluded by !**/rules.rs and included by **
  • crates/biome_configuration/src/generated/linter_options_check.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_diagnostics_categories/src/categories.rs is excluded by !**/categories.rs and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noArbitraryValue/invalid.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noArbitraryValue/invalid_functions.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noArbitraryValue/valid.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noArbitraryValue/valid_functions.jsx.snap is excluded by !**/*.snap and included by **
  • packages/@biomejs/backend-jsonrpc/src/workspace.ts is excluded by !**/backend-jsonrpc/src/workspace.ts and included by **
  • packages/@biomejs/biome/configuration_schema.json is excluded by !**/configuration_schema.json and included by **
📒 Files selected for processing (11)
  • .changeset/add-no-arbitrary-value.md
  • crates/biome_js_analyze/Cargo.toml
  • crates/biome_js_analyze/src/lint/nursery/no_arbitrary_value.rs
  • crates/biome_js_analyze/tests/specs/nursery/noArbitraryValue/invalid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noArbitraryValue/invalid_functions.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noArbitraryValue/invalid_functions.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noArbitraryValue/valid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noArbitraryValue/valid_functions.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noArbitraryValue/valid_functions.options.json
  • crates/biome_rule_options/src/lib.rs
  • crates/biome_rule_options/src/no_arbitrary_value.rs

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_js_analyze/src/lint/nursery/no_arbitrary_value.rs`:
- Around line 42-60: Update the rule docs in no_arbitrary_value.rs to include
default values for the documented options ("attributes" and "functions") and
replace the bare JSON block under "## Options" with a proper JSON options block
paired with a code example using the use_options (or expect_diagnostic)
directive; specifically state the defaults (e.g., attributes default to
["class","className"], functions default to the listed defaults), add a
reproducible code snippet that demonstrates an arbitrary value (for example a
JSX line with clsx("w-[400px]")), and reference an accompanying *.options.json
that sets functions: ["clsx"] so the documentation and documentation codegen
include the example and defaults.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 81c93f1f-7f1e-49a1-b01a-9643721da167

📥 Commits

Reviewing files that changed from the base of the PR and between 1bde577 and 0fa9ebf.

⛔ Files ignored due to path filters (1)
  • crates/biome_js_analyze/tests/specs/nursery/noArbitraryValue/invalid_functions.jsx.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (4)
  • .changeset/add-no-arbitrary-value.md
  • crates/biome_js_analyze/src/lint/nursery/no_arbitrary_value.rs
  • crates/biome_js_analyze/tests/specs/nursery/noArbitraryValue/invalid_functions.jsx
  • crates/biome_rule_options/src/no_arbitrary_value.rs
✅ Files skipped from review due to trivial changes (1)
  • .changeset/add-no-arbitrary-value.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/biome_js_analyze/tests/specs/nursery/noArbitraryValue/invalid_functions.jsx
  • crates/biome_rule_options/src/no_arbitrary_value.rs

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/biome_js_analyze/src/lint/nursery/no_arbitrary_value.rs (1)

172-174: Avoid unwrap() on the offset conversion.

In practice the offset can't exceed u32::MAX (the parser would have failed long before) so this is mostly cosmetic, but a bare unwrap() here gives no context if it ever did fire. A short expect message — or TextSize::try_from(offset).unwrap_or(TextSize::from(u32::MAX)) — would age better.

♻️ Suggested tweak
 fn text_size(offset: usize) -> TextSize {
-    TextSize::from(u32::try_from(offset).unwrap())
+    TextSize::from(
+        u32::try_from(offset).expect("class offset within source file should fit into u32"),
+    )
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_analyze/src/lint/nursery/no_arbitrary_value.rs` around lines
172 - 174, Replace the bare unwrap in fn text_size(offset: usize) -> TextSize:
instead of u32::try_from(offset).unwrap() use a checked conversion with a clear
failure message or a safe fallback; e.g. call
u32::try_from(offset).expect("text_size: offset exceeds u32::MAX") and pass that
into TextSize::from, or use
TextSize::try_from(offset).unwrap_or(TextSize::from(u32::MAX)) so the conversion
in text_size() gives contextual diagnostics or a safe clamp rather than
panicking with no context.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_js_analyze/src/lint/nursery/no_arbitrary_value.rs`:
- Around line 65-67: The JSX example under the use_options doc block is actually
a violation (clsx("w-[400px]") given functions: ["clsx"]) so update the code
fence for that example to include expect_diagnostic: change the triple-backtick
tag from "jsx,use_options" to "jsx,use_options,expect_diagnostic" so the doctest
expects one diagnostic for the clsx call in no_arbitrary_value.rs's
documentation example.

---

Nitpick comments:
In `@crates/biome_js_analyze/src/lint/nursery/no_arbitrary_value.rs`:
- Around line 172-174: Replace the bare unwrap in fn text_size(offset: usize) ->
TextSize: instead of u32::try_from(offset).unwrap() use a checked conversion
with a clear failure message or a safe fallback; e.g. call
u32::try_from(offset).expect("text_size: offset exceeds u32::MAX") and pass that
into TextSize::from, or use
TextSize::try_from(offset).unwrap_or(TextSize::from(u32::MAX)) so the conversion
in text_size() gives contextual diagnostics or a safe clamp rather than
panicking with no context.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2b2422e4-9403-4c29-89af-03a6f963503a

📥 Commits

Reviewing files that changed from the base of the PR and between 0fa9ebf and 6218f70.

📒 Files selected for processing (1)
  • crates/biome_js_analyze/src/lint/nursery/no_arbitrary_value.rs

Comment on lines +65 to +67
/// ```jsx,use_options
/// <div className={clsx("w-[400px]")} />;
/// ```
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Mark the use_options example as expect_diagnostic.

With functions: ["clsx"] from the JSON options block above, clsx("w-[400px]") is exactly the case the rule is meant to catch — so the documented "valid"-looking example is actually a violation. The doc test will report a diagnostic, but the code block isn't tagged to expect one, leading to a snapshot mismatch and misleading documentation.

📝 Proposed tweak
-    /// ```jsx,use_options
+    /// ```jsx,use_options,expect_diagnostic
     /// <div className={clsx("w-[400px]")} />;
     /// ```

As per coding guidelines: "Invalid code examples in rule documentation must use the 'expect_diagnostic' code block property and emit exactly one diagnostic per snippet".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_analyze/src/lint/nursery/no_arbitrary_value.rs` around lines
65 - 67, The JSX example under the use_options doc block is actually a violation
(clsx("w-[400px]") given functions: ["clsx"]) so update the code fence for that
example to include expect_diagnostic: change the triple-backtick tag from
"jsx,use_options" to "jsx,use_options,expect_diagnostic" so the doctest expects
one diagnostic for the clsx call in no_arbitrary_value.rs's documentation
example.

@ematipico
Copy link
Copy Markdown
Member

ematipico commented Apr 27, 2026

NOTE: Whilst the original issue said to have a dedicated tailwindcss domain, as my first contribution it seemed a tad out of scope. Additionally, a potential future / related PR could be useCanonicalClasses, which would have an extra degree of complexity vs this one due to it having an autofix

Still, the rule is definitely tailored for Tailwind (docs, business logic, tests) so it must be part of the tailwind domain, and it should be called noTwArbitraryValue.

Also, since there's no source rule to check, we would appreciate if you could explain the business logic of the code, so that we know how to review your contribution.

Thank you!

@ematipico ematipico removed the M-Likely Agent Meta: this was likely an automated PR without a human in the loop label Apr 27, 2026
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 27, 2026

Merging this PR will not alter performance

✅ 249 untouched benchmarks


Comparing THEjacob1000:feat/no-arbitrary-value (39fea32) with main (9dd3271)

Open in CodSpeed

@dyc3
Copy link
Copy Markdown
Contributor

dyc3 commented Apr 27, 2026

The naming convention I'm using for other tailwind rules is useTailwind/noTailwind so the rule name should actually be noTailwindArbitraryValue

@ematipico
Copy link
Copy Markdown
Member

No strong opinions 👍

Copy link
Copy Markdown
Contributor

@dyc3 dyc3 left a comment

Choose a reason for hiding this comment

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

we should implement this for html as well.

///
pub NoArbitraryValue {
version: "next",
name: "noArbitraryValue",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
name: "noArbitraryValue",
name: "noTailwindArbitraryValue",

Comment on lines +69 to +74
pub NoArbitraryValue {
version: "next",
name: "noArbitraryValue",
language: "jsx",
recommended: false,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

needs to go in the tailwind domain

}

fn text_size(offset: usize) -> TextSize {
TextSize::from(u32::try_from(offset).unwrap())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

unwrap can panic

markup! { "Avoid arbitrary values in Tailwind CSS classes." },
)
.note(markup! {
"Arbitrary values bypass the design system. Use a design token instead."
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is "design token" the right wording for this? sounds kinda jargon-y

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I'd say it's both the right wording and kinda jargon-y, but gonna reword to make it more easily understood

@THEjacob1000 THEjacob1000 changed the title feat(lint): add nursery rule noArbitraryValue feat(lint): add nursery rule noTailwindArbitraryValue 🤖🤖🤖 Apr 27, 2026
@github-actions github-actions Bot added the L-HTML Language: HTML and super languages label Apr 27, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
crates/biome_rule_options/src/no_tailwind_arbitrary_value.rs (1)

49-73: Manual JsonSchema may drift from UseSortedClassesOptions.

The hand-rolled schema enumerates attributes and functions directly, so any future additions (or changes to validation) on UseSortedClassesOptions won't be reflected here unless someone remembers to update both places. Not a blocker — just worth a comment near the inner struct so future changes don't silently desync.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_rule_options/src/no_tailwind_arbitrary_value.rs` around lines 49
- 73, Add a brief developer note next to the NoTailwindArbitraryValueOptions
definition (or immediately above the impl schemars::JsonSchema / json_schema
function) stating that the JSON schema is hand-written and mirrors the fields of
UseSortedClassesOptions, and warning maintainers to update this schema whenever
UseSortedClassesOptions changes to avoid drift; reference the symbols
NoTailwindArbitraryValueOptions, impl schemars::JsonSchema, and
UseSortedClassesOptions in the comment so future editors know the relationship
and where to sync changes.
crates/biome_html_analyze/src/lint/nursery/no_tailwind_arbitrary_value.rs (1)

139-219: Consider extracting the shared scanning helpers.

class_ranges, text_size, push_arbitrary_value_range, push_modifier_range, and the inner Tailwind-candidate match in arbitrary_ranges are byte-for-byte duplicates of the helpers in crates/biome_js_analyze/src/lint/nursery/no_tailwind_arbitrary_value.rs. Lifting them into a small shared module (e.g. under biome_tailwind_parser or a new biome_tailwind_lint_utils helper) would keep both rules in sync if the Tailwind syntax tree evolves. Not a blocker for this PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_html_analyze/src/lint/nursery/no_tailwind_arbitrary_value.rs`
around lines 139 - 219, Duplicate Tailwind-scanning helpers exist here; extract
them into a shared helper module and call them from this file. Create a new
crate/module (e.g. biome_tailwind_lint_utils) that exports the helper functions
class_ranges, text_size, push_arbitrary_value_range, push_modifier_range and a
single helper that encapsulates the candidate-scanning logic used inside
arbitrary_ranges (e.g. scan_tailwind_arbitrary_ranges or
collect_arbitrary_ranges_from_parse) which performs the
parse.tree().candidates() loop and the match over
AnyTwFullCandidate/AnyTwCandidate; then replace the local definitions in this
file with imports and call that shared helper from arbitrary_ranges, adjusting
signatures to accept TokenText/text offsets and to return or push Vec<TextRange>
as needed (refer to arbitrary_ranges, class_ranges, text_size,
push_arbitrary_value_range, push_modifier_range, and the AnyTw* candidate types
to wire parameters correctly).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_html_analyze/src/lint/nursery/no_tailwind_arbitrary_value.rs`:
- Around line 44-62: Add a documentation example showing how to pass the
`attributes` option and a matching `use_options,expect_diagnostic` code snippet
for this rule (no_tailwind_arbitrary_value) so the options are testable; update
the doc comment in no_tailwind_arbitrary_value.rs to include a JSON options
block (if missing) and immediately below add an
html,use_options,expect_diagnostic fenced example that sets attributes to
["classList"] and includes a sample element (e.g. <div
classList="w-[400px]"></div>) that triggers the diagnostic, ensuring the default
value statement remains present.
- Around line 130-137: The current check in is_html_class_attribute uses
case-sensitive comparisons and will miss ASCII-case variants; update
is_html_class_attribute to use name.eq_ignore_ascii_case("class") and change the
attributes iteration to compare attribute.as_ref().eq_ignore_ascii_case(name).
Also update the similar lookup logic in HtmlAttributeList::find_by_name to
perform ASCII case-insensitive comparisons, and fix the case-sensitive "class"
and "style" checks in no_duplicate_attributes.rs (replace == "class"/== "style"
with eq_ignore_ascii_case) so attribute name matching follows the HTML ASCII
case-insensitive rule.

---

Nitpick comments:
In `@crates/biome_html_analyze/src/lint/nursery/no_tailwind_arbitrary_value.rs`:
- Around line 139-219: Duplicate Tailwind-scanning helpers exist here; extract
them into a shared helper module and call them from this file. Create a new
crate/module (e.g. biome_tailwind_lint_utils) that exports the helper functions
class_ranges, text_size, push_arbitrary_value_range, push_modifier_range and a
single helper that encapsulates the candidate-scanning logic used inside
arbitrary_ranges (e.g. scan_tailwind_arbitrary_ranges or
collect_arbitrary_ranges_from_parse) which performs the
parse.tree().candidates() loop and the match over
AnyTwFullCandidate/AnyTwCandidate; then replace the local definitions in this
file with imports and call that shared helper from arbitrary_ranges, adjusting
signatures to accept TokenText/text offsets and to return or push Vec<TextRange>
as needed (refer to arbitrary_ranges, class_ranges, text_size,
push_arbitrary_value_range, push_modifier_range, and the AnyTw* candidate types
to wire parameters correctly).

In `@crates/biome_rule_options/src/no_tailwind_arbitrary_value.rs`:
- Around line 49-73: Add a brief developer note next to the
NoTailwindArbitraryValueOptions definition (or immediately above the impl
schemars::JsonSchema / json_schema function) stating that the JSON schema is
hand-written and mirrors the fields of UseSortedClassesOptions, and warning
maintainers to update this schema whenever UseSortedClassesOptions changes to
avoid drift; reference the symbols NoTailwindArbitraryValueOptions, impl
schemars::JsonSchema, and UseSortedClassesOptions in the comment so future
editors know the relationship and where to sync changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 81b1aa6a-7ce6-4fd8-b00f-27d9911d48a8

📥 Commits

Reviewing files that changed from the base of the PR and between 6218f70 and 21f3e86.

⛔ Files ignored due to path filters (13)
  • Cargo.lock is excluded by !**/*.lock and included by **
  • crates/biome_configuration/src/analyzer/linter/rules.rs is excluded by !**/rules.rs and included by **
  • crates/biome_configuration/src/generated/domain_selector.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_configuration/src/generated/linter_options_check.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_diagnostics_categories/src/categories.rs is excluded by !**/categories.rs and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noTailwindArbitraryValue/invalid.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noTailwindArbitraryValue/valid.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noTailwindArbitraryValue/invalid.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noTailwindArbitraryValue/invalid_functions.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noTailwindArbitraryValue/valid.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noTailwindArbitraryValue/valid_functions.jsx.snap is excluded by !**/*.snap and included by **
  • packages/@biomejs/backend-jsonrpc/src/workspace.ts is excluded by !**/backend-jsonrpc/src/workspace.ts and included by **
  • packages/@biomejs/biome/configuration_schema.json is excluded by !**/configuration_schema.json and included by **
📒 Files selected for processing (15)
  • .changeset/add-no-tailwind-arbitrary-value.md
  • crates/biome_html_analyze/Cargo.toml
  • crates/biome_html_analyze/src/lint/nursery/no_tailwind_arbitrary_value.rs
  • crates/biome_html_analyze/tests/specs/nursery/noTailwindArbitraryValue/invalid.html
  • crates/biome_html_analyze/tests/specs/nursery/noTailwindArbitraryValue/invalid.options.json
  • crates/biome_html_analyze/tests/specs/nursery/noTailwindArbitraryValue/valid.html
  • crates/biome_js_analyze/src/lint/nursery/no_tailwind_arbitrary_value.rs
  • crates/biome_js_analyze/tests/specs/nursery/noTailwindArbitraryValue/invalid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noTailwindArbitraryValue/invalid_functions.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noTailwindArbitraryValue/invalid_functions.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noTailwindArbitraryValue/valid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noTailwindArbitraryValue/valid_functions.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noTailwindArbitraryValue/valid_functions.options.json
  • crates/biome_rule_options/src/lib.rs
  • crates/biome_rule_options/src/no_tailwind_arbitrary_value.rs
✅ Files skipped from review due to trivial changes (9)
  • crates/biome_html_analyze/tests/specs/nursery/noTailwindArbitraryValue/valid.html
  • crates/biome_js_analyze/tests/specs/nursery/noTailwindArbitraryValue/valid_functions.options.json
  • crates/biome_rule_options/src/lib.rs
  • crates/biome_js_analyze/tests/specs/nursery/noTailwindArbitraryValue/invalid_functions.options.json
  • crates/biome_js_analyze/tests/specs/nursery/noTailwindArbitraryValue/valid.jsx
  • .changeset/add-no-tailwind-arbitrary-value.md
  • crates/biome_js_analyze/tests/specs/nursery/noTailwindArbitraryValue/valid_functions.jsx
  • crates/biome_html_analyze/tests/specs/nursery/noTailwindArbitraryValue/invalid.options.json
  • crates/biome_html_analyze/tests/specs/nursery/noTailwindArbitraryValue/invalid.html

@THEjacob1000
Copy link
Copy Markdown
Author

THEjacob1000 commented Apr 28, 2026

You shouldn't. That's been there to catch OpenClaw bots. It's a naive but effective honeypot.

ah ok then, I saw it in the agents.md and couldn't find it anywhere in the contribution docs so thought I had to add it if I was using AI

Copy link
Copy Markdown
Contributor

@dyc3 dyc3 left a comment

Choose a reason for hiding this comment

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

the unrelated changes to other lint rules are technically valid, but they pollute what should be a rather clean PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

unrelated change

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

unrelated change

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

unrelated change

declare_lint_rule! {
/// Disallow arbitrary values in Tailwind CSS utility classes.
///
/// Arbitrary values (e.g. `w-[400px]`, `text-[#555]`, `[color:red]`) bypass
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[color:red] is technically an arbitrary candidate, not a value. But, its likely that users don't really care about that distinction. should we call the rule noTailwindArbitrary? I'd like to know what you think.

Copy link
Copy Markdown
Author

@THEjacob1000 THEjacob1000 May 5, 2026

Choose a reason for hiding this comment

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

I'd lean toward keeping noTailwindArbitraryValue to match the upstream eslint-plugin-tailwindcss/no-arbitrary-value source and because 'arbitrary value' is the term I believe most users reach for. Happy to clarify in the docs/diagnostic that it also covers arbitrary properties like [color:red]. Open to changing it if you feel strongly though.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's make sure the docs mention that it covers arbitrary properties.

///
/// Controls which attributes and utility functions are checked for arbitrary values.
#[derive(Default, Clone, Debug, Eq, PartialEq)]
pub struct NoTailwindArbitraryValueOptions(UseSortedClassesOptions);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not sure how I feel about reusing another rule's options like this. It's not something we normally do. cc @ematipico thoughts?

I think long term we'll have dedicated tailwind configuration that can be shared across these rules, but that's not something you necessarily need to worry about for this PR.

@dyc3
Copy link
Copy Markdown
Contributor

dyc3 commented Apr 30, 2026

Also, there's prior art here: https://github.com/francoismassart/eslint-plugin-tailwindcss/blob/master/docs/rules/no-arbitrary-value.md

So we should add the appropriate rule sources

…evert unrelated changes in noTailwindArbitraryValue

- Add EslintTailwindcss variant to RuleSource (francoismassart/eslint-plugin-tailwindcss prior art)
- Add sources field to both HTML and JS noTailwindArbitraryValue rules
- Add contains('[') fast-path in scan_tailwind_arbitrary_ranges to skip parse on class strings with no brackets (fixes ~11% CodSpeed regression on Wikipedia HTML benchmarks)
- Revert unrelated eq_ignore_ascii_case changes to noDuplicateClasses, noDuplicateAttributes, noInlineStyles and their test fixtures
@github-actions github-actions Bot added the A-CLI Area: CLI label May 5, 2026
@THEjacob1000 THEjacob1000 requested a review from dyc3 May 5, 2026 00:59
Copy link
Copy Markdown
Contributor

@dyc3 dyc3 left a comment

Choose a reason for hiding this comment

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

I feel like your agent is trying to compensate too much for bugs in the parser to make the rule work.

also, this still needs an answer: #10094 (comment)

Comment thread crates/biome_tailwind_parser/src/lint_utils.rs Outdated
Comment thread crates/biome_tailwind_parser/src/lint_utils.rs Outdated
Comment thread crates/biome_tailwind_parser/src/lint_utils.rs Outdated
Comment thread crates/biome_tailwind_parser/src/lint_utils.rs Outdated
Comment thread crates/biome_tailwind_parser/src/lint_utils.rs Outdated
Copy link
Copy Markdown
Contributor

@dyc3 dyc3 left a comment

Choose a reason for hiding this comment

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

Looking much better! Just a few more things I noticed.

Comment on lines +20 to +23
results.push(TextRange::new(
content_start + range.start(),
content_start + range.end(),
));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
results.push(TextRange::new(
content_start + range.start(),
content_start + range.end(),
));
results.push(range + content_start);

Comment on lines +43 to +46
results.push(TextRange::new(
content_start + range.start(),
content_start + range.end(),
));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
results.push(TextRange::new(
content_start + range.start(),
content_start + range.end(),
));
results.push(range + content_start);

Comment on lines +5 to +8
/// Collects text ranges of all arbitrary values in the given candidate list.
///
/// `content_start` is the source offset of the first character of the parsed
/// string, used to translate parse-relative ranges into source ranges.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: mention this is the business logic of the rule.

declare_lint_rule! {
/// Disallow arbitrary values in Tailwind CSS utility classes.
///
/// Arbitrary values (e.g. `w-[400px]`, `text-[#555]`, `[color:red]`) bypass
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's make sure the docs mention that it covers arbitrary properties.

///
/// Controls which attributes and utility functions are checked for arbitrary values.
#[derive(Default, Clone, Debug, Eq, PartialEq)]
pub struct NoTailwindArbitraryValueOptions(UseSortedClassesOptions);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

After thinking about it for a while, the shared options is probably a footgun. Lets duplicate the options for now, even though they are the same.

@THEjacob1000
Copy link
Copy Markdown
Author

NOTE: Whilst the original issue said to have a dedicated tailwindcss domain, as my first contribution it seemed a tad out of scope. Additionally, a potential future / related PR could be useCanonicalClasses, which would have an extra degree of complexity vs this one due to it having an autofix

After thinking about it useCanonicalClasses would likely just be an unsafe autofix for this rule as it's just replacing arbitrary values with tailwind equivalents

Comment on lines +97 to +100
let sorted_options = UseSortedClassesOptions {
attributes: options.attributes.clone(),
functions: options.functions.clone(),
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: Don't use UseSortedClassesOptions at all

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

introduced ClassStringOptions in any_class_string_like.rs to allow the rule to pass its own options directly.

Comment on lines +36 to +44
impl Deserializable for NoTailwindArbitraryValueOptions {
fn deserialize(
ctx: &mut impl DeserializationContext,
value: &impl DeserializableValue,
name: &str,
) -> Option<Self> {
value.deserialize(ctx, NoTailwindArbitraryValueOptionsVisitor, name)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

shouldn't be needed

Comment on lines +46 to +53
struct NoTailwindArbitraryValueOptionsVisitor;

impl DeserializationVisitor for NoTailwindArbitraryValueOptionsVisitor {
type Output = NoTailwindArbitraryValueOptions;

const EXPECTED_TYPE: DeserializableTypes = DeserializableTypes::MAP;

fn visit_map(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

shouldn't be needed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

also, why did this snapshot change?

Copy link
Copy Markdown
Author

@THEjacob1000 THEjacob1000 May 7, 2026

Choose a reason for hiding this comment

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

The snapshot changed because of the text_trimmed() fix. Without it, calling .to_string() on a JsStaticMemberExpression tag includes surrounding whitespace trivia, so tw.div ends up as " tw.div" and misses the string comparison against the configured function names. The fix makes tw.div\...` get detected correctly by useSortedClasses. This new diagnostic is intentional.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

if necessary I'm happy to create an additional patch changeset describing this fix

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ah, ok. seems good to me, but maybe include another changeset for how it affects useSortedClasses

Comment thread crates/biome_html_analyze/src/lint/nursery/no_tailwind_arbitrary_value.rs Outdated
@dyc3
Copy link
Copy Markdown
Contributor

dyc3 commented May 8, 2026

that and the changeset i mentioned #10094 (comment)

@dyc3 dyc3 self-assigned this May 9, 2026
@dyc3
Copy link
Copy Markdown
Contributor

dyc3 commented May 10, 2026

Please avoid merging main to keep the branch up to date (unless you are resolving merge conflicts). we have to manually approve CI runs because you are a first time contributor.

@dyc3 dyc3 requested a review from ematipico May 10, 2026 12:59
@THEjacob1000
Copy link
Copy Markdown
Author

Please avoid merging main to keep the branch up to date (unless you are resolving merge conflicts). we have to manually approve CI runs because you are a first time contributor.

@dyc3 my bad I didn't know what the process was to get it merged after initial approval and thought I had to keep the branch up to date

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-CLI Area: CLI A-Diagnostic Area: diagnostocis A-Linter Area: linter A-Parser Area: parser A-Project Area: project L-HTML Language: HTML and super languages L-JavaScript Language: JavaScript and super languages L-Tailwind Language: Tailwind CSS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants