Skip to content

Commit

Permalink
feat(grit): implement disregarded snippet nodes (#4084)
Browse files Browse the repository at this point in the history
  • Loading branch information
arendjr authored Sep 25, 2024
1 parent f3cfa8a commit e184d45
Show file tree
Hide file tree
Showing 19 changed files with 272 additions and 20 deletions.
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
/crates/biome_js_analyze/src/{lint,assists,syntax}/*.rs linguist-generated=true text=auto eol=lf
/crates/biome_js_analyze/src/options.rs linguist-generated=true text=auto eol=lf
/crates/biome_js_analyze/src/registry.rs linguist-generated=true text=auto eol=lf
# Grit
/crates/biome_grit_patterns/src/grit_target_language/*/constants.rs linguist-generated=true text=auto eol=lf

# Other
/crates/biome_unicode_table/src/tables.rs linguist-generated=true text=auto eol=lf
Expand Down
7 changes: 3 additions & 4 deletions crates/biome_grit_patterns/src/grit_node_patterns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,9 @@ impl Matcher<GritQueryContext> for GritNodePattern {
let mut cur_state = running_state.clone();

let res = pattern.execute(
&if let Some(child) = node.child_by_slot_index(*slot_index) {
GritResolvedPattern::from_node_binding(child)
} else {
GritResolvedPattern::from_empty_binding(node.clone(), *slot_index)
&match node.child_by_slot_index(*slot_index) {
Some(child) => GritResolvedPattern::from_node_binding(child),
None => GritResolvedPattern::from_empty_binding(node.clone(), *slot_index),
},
&mut cur_state,
context,
Expand Down
55 changes: 55 additions & 0 deletions crates/biome_grit_patterns/src/grit_target_language.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,17 @@ macro_rules! generate_target_language {
}
}

pub fn is_disregarded_snippet_field(
&self,
kind: GritTargetSyntaxKind,
slot_index: u32,
node: Option<GritTargetNode<'_>>,
) -> bool {
match self {
$(Self::$language(lang) => lang.is_disregarded_snippet_field(kind, slot_index, node)),+
}
}

pub fn get_equivalence_class(
&self,
kind: GritTargetSyntaxKind,
Expand Down Expand Up @@ -266,6 +277,44 @@ trait GritTargetLanguageImpl {
false
}

/// Ordinarily, we want to match on all possible fields, including the absence of nodes within a field.
/// e.g., `my_function()` should not match `my_function(arg)`.
///
/// However, some fields are trivial or not expected to be part of the snippet, and should be disregarded.
/// For example, in JavaScript, we want to match both `function name() {}` and `async function name() {}` with the same snippet.
///
/// You can still match on the presence/absence of the field in the snippet by including a metavariable and checking its value.
/// For example, in JavaScript:
/// ```grit
/// `$async func name(args)` where $async <: .
/// ```
///
/// This method allows you to specify fields that should be (conditionally) disregarded in snippets.
/// The actual value of the field from the snippet, if any, is passed in as the third argument.
///
/// Note that if a field is always disregarded, you can still switch to ast_node syntax to match on these fields.
/// For example, in react_to_hooks we match on `arrow_function` and capture `$parenthesis` for inspection.
///
/// ```grit
/// arrow_function(parameters=$props, $body, $parenthesis) where {
/// $props <: contains or { `props`, `inputProps` },
/// $body <: not contains `props`,
/// if ($parenthesis <: .) {
/// $props => `()`
/// } else {
/// $props => .
/// }
/// }
/// ```
fn is_disregarded_snippet_field(
&self,
_kind: GritTargetSyntaxKind,
_slot_index: u32,
_node: Option<GritTargetNode<'_>>,
) -> bool {
false
}

/// Returns an optional "equivalence class" for the given syntax kind.
///
/// Equivalence classes allow leaf nodes to be classified as being equal,
Expand Down Expand Up @@ -353,3 +402,9 @@ fn normalize_quoted_string(string: &str) -> Option<&str> {
// Strip the quotes, regardless of type:
(string.len() >= 2).then(|| &string[1..string.len() - 1])
}

#[derive(Debug, Clone)]
enum DisregardedSlotCondition {
Always,
OnlyIf(&'static [&'static str]),
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
mod constants;

use super::{
normalize_quoted_string, GritTargetLanguageImpl, LeafEquivalenceClass, LeafNormalizer,
normalize_quoted_string, DisregardedSlotCondition, GritTargetLanguageImpl,
LeafEquivalenceClass, LeafNormalizer,
};
use crate::{
grit_target_node::{GritTargetNode, GritTargetSyntaxKind},
CompileError,
};
use crate::{grit_target_node::GritTargetSyntaxKind, CompileError};
use biome_js_syntax::{JsLanguage, JsSyntaxKind};
use biome_rowan::{RawSyntaxKind, SyntaxKindSet};
use constants::DISREGARDED_SNIPPET_SLOTS;

const COMMENT_KINDS: SyntaxKindSet<JsLanguage> =
SyntaxKindSet::from_raw(RawSyntaxKind(JsSyntaxKind::COMMENT as u16)).union(
Expand Down Expand Up @@ -135,6 +142,30 @@ impl GritTargetLanguageImpl for JsTargetLanguage {
})
}

fn is_disregarded_snippet_field(
&self,
kind: GritTargetSyntaxKind,
slot_index: u32,
node: Option<GritTargetNode<'_>>,
) -> bool {
DISREGARDED_SNIPPET_SLOTS.iter().any(
|(disregarded_kind, disregarded_slot_index, condition)| {
if GritTargetSyntaxKind::from(*disregarded_kind) != kind
|| *disregarded_slot_index != slot_index
{
return false;
}

match condition {
DisregardedSlotCondition::Always => true,
DisregardedSlotCondition::OnlyIf(node_texts) => node_texts.iter().any(|text| {
*text == node.as_ref().map(|node| node.text()).unwrap_or_default()
}),
}
},
)
}

fn get_equivalence_class(
&self,
kind: GritTargetSyntaxKind,
Expand Down

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

25 changes: 11 additions & 14 deletions crates/biome_grit_patterns/src/pattern_compiler/snippet_compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ fn pattern_from_node(
return Ok(metavariable);
}

if node.slots().is_none() {
let Some(slots) = node.slots() else {
let content = node.text();
let lang = &context.compilation.lang;
let pattern = if let Some(regex_pattern) = lang
Expand All @@ -227,22 +227,19 @@ fn pattern_from_node(
};

return Ok(pattern);
}
};

let kind = node.kind();
let args = node
.slots()
.map(|slots| {
// TODO: Implement filtering for disregarded snippet fields.
// Implementing this will make it more convenient to match
// CST nodes without needing to match all the trivia in the
// snippet (if I understand correctly).
slots
.map(|slot| pattern_arg_from_slot(slot, context_range, range_map, context, is_rhs))
.collect::<Result<Vec<GritNodePatternArg>, CompileError>>()
let args = slots
.filter(|slot| {
!context.compilation.lang.is_disregarded_snippet_field(
kind,
slot.index(),
node.child_by_slot_index(slot.index()),
)
})
.transpose()?
.unwrap_or_default();
.map(|slot| pattern_arg_from_slot(slot, context_range, range_map, context, is_rhs))
.collect::<Result<Vec<GritNodePatternArg>, CompileError>>()?;

Ok(Pattern::AstNode(Box::new(GritNodePattern { kind, args })))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`async function foo() {}`
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
source: crates/biome_grit_patterns/tests/spec_tests.rs
expression: asyncFunctionDoesntMatchNonAsync
---
SnapshotResult {
messages: [],
matched_ranges: [
"3:1-3:23",
],
rewritten_files: [],
created_files: [],
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
function foo() {}
function bar(){}
async function foo(){}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`function foo() {}`
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
source: crates/biome_grit_patterns/tests/spec_tests.rs
expression: functionMatchesAsync
---
SnapshotResult {
messages: [],
matched_ranges: [
"1:1-1:18",
"3:1-3:23",
],
rewritten_files: [],
created_files: [],
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
function foo() {}
function bar(){}
async function foo(){}
1 change: 1 addition & 0 deletions crates/biome_grit_patterns/tests/specs/ts/typeAlias.grit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`type $T = $U`
13 changes: 13 additions & 0 deletions crates/biome_grit_patterns/tests/specs/ts/typeAlias.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
source: crates/biome_grit_patterns/tests/spec_tests.rs
expression: typeAlias
---
SnapshotResult {
messages: [],
matched_ranges: [
"1:1-1:19",
"2:1-2:19",
],
rewritten_files: [],
created_files: [],
}
2 changes: 2 additions & 0 deletions crates/biome_grit_patterns/tests/specs/ts/typeAlias.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
type Foo = string;
type Bar = number;
10 changes: 10 additions & 0 deletions xtask/codegen/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use super::{
use crate::generate_node_factory::generate_node_factory;
use crate::generate_nodes_mut::generate_nodes_mut;
use crate::generate_syntax_factory::generate_syntax_factory;
use crate::generate_target_language_constants::generate_target_language_constants;
use crate::js_kinds_src::{
AstEnumSrc, AstListSeparatorConfiguration, AstListSrc, AstNodeSrc, TokenKind,
};
Expand Down Expand Up @@ -76,6 +77,9 @@ pub(crate) fn generate_syntax(ast: AstSrc, mode: &Mode, language_kind: LanguageK
.join("crates")
.join(language_kind.factory_crate_name())
.join("src/generated");
let target_language_path = project_root()
.join("crates/biome_grit_patterns/src/grit_target_language")
.join(language_kind.grit_target_language_module_name());

let kind_src = language_kind.kinds();

Expand Down Expand Up @@ -103,6 +107,12 @@ pub(crate) fn generate_syntax(ast: AstSrc, mode: &Mode, language_kind: LanguageK
let contents = generate_macros(&ast, language_kind)?;
update(ast_macros_file.as_path(), &contents, mode)?;

if language_kind.supports_grit() {
let target_language_constants_file = target_language_path.join("constants.rs");
let contents = generate_target_language_constants(&ast, language_kind)?;
update(target_language_constants_file.as_path(), &contents, mode)?;
}

Ok(())
}

Expand Down
49 changes: 49 additions & 0 deletions xtask/codegen/src/generate_target_language_constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use crate::{
js_kinds_src::{AstSrc, Field},
language_kind::LanguageKind,
};
use biome_string_case::Case;
use xtask::Result;

pub fn generate_target_language_constants(
ast: &AstSrc,
_language_kind: LanguageKind,
) -> Result<String> {
let disregarded_slots: Vec<String> = ast
.nodes
.iter()
.flat_map(|node| {
let node_kind = Case::Constant.convert(node.name.as_str());
node.fields
.iter()
.enumerate()
.filter_map(|(index, field)| match field {
Field::Token { name, optional, .. } => Some((index, name, optional)),
Field::Node { .. } => None,
})
// TODO: We might want to move this to `js_kinds_src.rs` when we
// start supporting other languages with Grit.
.filter_map(|(index, name, optional)| match (name.as_str(), optional) {
("async", true) => Some(format!("({node_kind}, {index}, OnlyIf(&[\"\"])),")),
(";", true) => Some(format!("({node_kind}, {index}, Always),")),
_ => None,
})
.collect::<Vec<_>>()
})
.collect();
let disregarded_slots = disregarded_slots.join("\n ");

let result = format!(
"use crate::grit_target_language::DisregardedSlotCondition::{{self, *}};
use biome_js_syntax::JsSyntaxKind::{{self, *}};
pub(crate) const DISREGARDED_SNIPPET_SLOTS: &[(JsSyntaxKind, u32, DisregardedSlotCondition)] = &[
{disregarded_slots}
];
"
);

let pretty = xtask::reformat(result)?;

Ok(pretty)
}
8 changes: 8 additions & 0 deletions xtask/codegen/src/language_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ impl LanguageKind {
format!("biome_{self}_factory")
}

pub fn grit_target_language_module_name(&self) -> String {
format!("{self}_target_language")
}

pub fn kinds(&self) -> KindsSrc {
match self {
LanguageKind::Js => JS_KINDS_SRC,
Expand All @@ -156,4 +160,8 @@ impl LanguageKind {
LanguageKind::Markdown => include_str!("../markdown.ungram"),
}
}

pub fn supports_grit(&self) -> bool {
matches!(self, Self::Js)
}
}
Loading

0 comments on commit e184d45

Please sign in to comment.