Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/thirty-lines-sit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": patch
---

Fixed Grit queries that use native Biome AST node names with the native field names that are in our `.ungram` grammar files. Queries such as `JsConditionalExpression(consequent = $cons, alternate = $alt)` now compile successfully in `biome search` and grit plugins.
9 changes: 8 additions & 1 deletion .claude/skills/changeset/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@ Use this skill when a PR introduces user-facing changes that require a changeset
just new-changeset-empty
```

The command will create an empty file `.changeset/`. Edit it directly to add detail.
The command will create a file in `.changeset/`. Edit it directly to add detail.

Note that the file will *not* be literally empty. It will have this content:

```markdown
---
---
```

> Requires `pnpm` — run `pnpm i` from repo root first.

Expand Down
43 changes: 43 additions & 0 deletions crates/biome_cli/tests/commands/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3353,6 +3353,49 @@ fn check_json_plugin() {
));
}

#[test]
fn check_js_plugin_with_native_field_names() {
let fs = MemoryFileSystem::default();
let mut console = BufferConsole::default();

fs.insert(
Utf8PathBuf::from("biome.json"),
br#"{
"plugins": ["noConditionalAlternate.grit"],
"formatter": {
"enabled": false
}
}"#,
);

fs.insert(
Utf8PathBuf::from("noConditionalAlternate.grit"),
br#"language js

JsConditionalExpression(consequent = $cons, alternate = $alt) where {
register_diagnostic(span = $alt, message = "Avoid the alternate branch.", severity = "error")
}
"#,
);

let file_path = "file.js";
fs.insert(file_path.into(), br#"condition ? consequent : alternate;"#);

let (fs, result) = run_cli_with_server_workspace(
fs,
&mut console,
Args::from(["check", file_path].as_slice()),
);

assert_cli_snapshot(SnapshotPayload::new(
module_path!(),
"check_js_plugin_with_native_field_names",
fs,
console,
result,
));
}

#[test]
fn check_plugin_diagnostic_offset_in_vue_file() {
let fs = MemoryFileSystem::default();
Expand Down
34 changes: 34 additions & 0 deletions crates/biome_cli/tests/commands/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const CSS_FILE_CONTENT: &str = r#"div {
// existing tests.
const JS_FILE_CONTENT: &str = r#"const a = 'foo';"#;

const JS_CONDITIONAL_FILE_CONTENT: &str = r#"condition ? consequent : alternate;"#;

const JSON_FILE_CONTENT: &str = r#"{
"name": "test",
"version": "1.0.0"
Expand Down Expand Up @@ -106,6 +108,38 @@ fn search_js_pattern() {
));
}

#[test]
fn search_js_pattern_with_native_field_names() {
let fs = MemoryFileSystem::default();
let mut console = BufferConsole::default();

let file_path = Utf8Path::new("file.js");
fs.insert(file_path.into(), JS_CONDITIONAL_FILE_CONTENT.as_bytes());

let (fs, result) = run_cli(
fs,
&mut console,
Args::from(
[
"search",
"JsConditionalExpression(consequent = $cons, alternate = $alt)",
file_path.as_str(),
]
.as_slice(),
),
);

assert!(result.is_ok(), "run_cli returned {result:?}");

assert_cli_snapshot(SnapshotPayload::new(
module_path!(),
"search_js_pattern_with_native_field_names",
fs,
console,
result,
));
}

#[test]
fn search_js_pattern_skips_css_files() {
let fs = MemoryFileSystem::default();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
source: crates/biome_cli/tests/snap_test.rs
expression: redactor(content)
---
## `biome.json`

```json
{
"plugins": ["noConditionalAlternate.grit"],
"formatter": {
"enabled": false
}
}
```

## `file.js`

```js
condition ? consequent : alternate;
```

## `noConditionalAlternate.grit`

```grit
language js

JsConditionalExpression(consequent = $cons, alternate = $alt) where {
register_diagnostic(span = $alt, message = "Avoid the alternate branch.", severity = "error")
}

```

# Termination Message

```block
check ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Some errors were emitted while running checks.



```

# Emitted Messages

```block
file.js:1:26 plugin ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Avoid the alternate branch.

> 1 │ condition ? consequent : alternate;
│ ^^^^^^^^^


```

```block
Checked 1 file in <TIME>. No fixes applied.
Found 1 error.
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
source: crates/biome_cli/tests/snap_test.rs
expression: redactor(content)
---
## `file.js`

```js
condition ? consequent : alternate;
```

# Emitted Messages

```block
file.js:1:1 search ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

1 │ condition ? consequent : alternate;

```

```block
Searched 1 file in <TIME>. Found 1 match.
```
48 changes: 44 additions & 4 deletions crates/biome_grit_patterns/src/grit_target_language.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ use crate::grit_json_parser::GritJsonParser;
use crate::grit_target_node::{GritTargetNode, GritTargetSyntaxKind};
use crate::grit_tree::GritTargetTree;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GritNodePatternSource {
LegacyTreeSitter,
Native,
}

/// Generates the `GritTargetLanguage` enum.
///
/// This enum contains a variant for every language that we support running Grit
Expand Down Expand Up @@ -111,15 +117,35 @@ macro_rules! generate_target_language {
}
}

pub fn resolve_node_pattern_name(
&self,
name: &str,
) -> Option<(GritTargetSyntaxKind, GritNodePatternSource)> {
match self {
$(Self::$language(lang) => lang
.legacy_kind_by_name(name)
.map(|kind| (kind.into(), GritNodePatternSource::LegacyTreeSitter))
.or_else(|| {
lang.native_kind_by_name(name)
.map(|kind| (kind.into(), GritNodePatternSource::Native))
})),+
}
}

pub fn name_for_kind(&self, name: GritTargetSyntaxKind) -> &'static str {
match self {
$(Self::$language(lang) => lang.name_for_kind(name)),+
}
}

pub fn named_slots_for_kind(&self, kind: GritTargetSyntaxKind) -> &'static [(&'static str, u32)] {
pub fn named_slots_for_node(
&self,
node_name: &str,
kind: GritTargetSyntaxKind,
source: GritNodePatternSource,
) -> &'static [(&'static str, u32)] {
match self {
$(Self::$language(lang) => lang.named_slots_for_kind(kind)),+
$(Self::$language(lang) => lang.named_slots_for_node(node_name, kind, source)),+
}
}

Expand Down Expand Up @@ -319,7 +345,16 @@ trait GritTargetLanguageImpl {
/// For compatibility with existing Grit snippets (as well as the online
/// Grit playground), node names should be aligned with TreeSitter's
/// `ts_language_symbol_for_name()`.
fn kind_by_name(&self, node_name: &str) -> Option<Self::Kind>;
fn kind_by_name(&self, node_name: &str) -> Option<Self::Kind> {
self.legacy_kind_by_name(node_name)
.or_else(|| self.native_kind_by_name(node_name))
}

fn legacy_kind_by_name(&self, _node_name: &str) -> Option<Self::Kind> {
None
}

fn native_kind_by_name(&self, node_name: &str) -> Option<Self::Kind>;

/// Returns the node name for a given syntax kind.
///
Expand All @@ -335,7 +370,12 @@ trait GritTargetLanguageImpl {
/// For compatibility with existing Grit snippets (as well as the online
/// Grit playground), node names should be aligned with TreeSitter's
/// `ts_language_field_name_for_id()`.
fn named_slots_for_kind(&self, kind: GritTargetSyntaxKind) -> &'static [(&'static str, u32)];
fn named_slots_for_node(
&self,
node_name: &str,
kind: GritTargetSyntaxKind,
source: GritNodePatternSource,
) -> &'static [(&'static str, u32)];

/// Strings that provide context for parsing snippets.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ mod constants;
pub mod generated_mappings;

use super::{
DisregardedSlotCondition, GritTargetLanguageImpl, LeafEquivalenceClass, LeafNormalizer,
normalize_quoted_string,
DisregardedSlotCondition, GritNodePatternSource, GritTargetLanguageImpl, LeafEquivalenceClass,
LeafNormalizer, normalize_quoted_string,
};
use crate::{
CompileError,
Expand All @@ -12,7 +12,7 @@ use crate::{
use biome_css_syntax::{CssLanguage, CssSyntaxKind};
use biome_rowan::{RawSyntaxKind, SyntaxKindSet};
use constants::DISREGARDED_SNIPPET_SLOTS;
use generated_mappings::kind_by_name;
use generated_mappings::{kind_by_name, native_slots_for_name};

const COMMENT_KINDS: SyntaxKindSet<CssLanguage> =
SyntaxKindSet::from_raw(RawSyntaxKind(CssSyntaxKind::COMMENT as u16)).union(
Expand All @@ -33,7 +33,7 @@ impl GritTargetLanguageImpl for CssTargetLanguage {
/// Returns the syntax kind for a node by name.
///
/// Supports native Biome AST patterns for full language coverage.
fn kind_by_name(&self, node_name: &str) -> Option<CssSyntaxKind> {
fn native_kind_by_name(&self, node_name: &str) -> Option<CssSyntaxKind> {
kind_by_name(node_name)
}

Expand All @@ -52,9 +52,16 @@ impl GritTargetLanguageImpl for CssTargetLanguage {
/// For compatibility with existing Grit snippets (as well as the online
/// Grit playground), node names should be aligned with TreeSitter's
/// `ts_language_field_name_for_id()`.
fn named_slots_for_kind(&self, _kind: GritTargetSyntaxKind) -> &'static [(&'static str, u32)] {
// TODO: See [super::JsTargetLanguage::named_slots_for_kind()].
&[]
fn named_slots_for_node(
&self,
node_name: &str,
_kind: GritTargetSyntaxKind,
source: GritNodePatternSource,
) -> &'static [(&'static str, u32)] {
match source {
GritNodePatternSource::LegacyTreeSitter => &[],
GritNodePatternSource::Native => native_slots_for_name(node_name),
}
}

fn snippet_context_strings(&self) -> &[(&'static str, &'static str)] {
Expand Down
Loading
Loading