Skip to content

Commit

Permalink
feat(grit): support Grit queries targeting CSS (#4444)
Browse files Browse the repository at this point in the history
  • Loading branch information
arendjr authored Nov 1, 2024
1 parent 7c16779 commit ad8d7bd
Show file tree
Hide file tree
Showing 17 changed files with 382 additions and 58 deletions.
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.

2 changes: 2 additions & 0 deletions crates/biome_grit_patterns/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ version = "0.0.1"
[dependencies]
biome_analyze = { workspace = true }
biome_console = { workspace = true }
biome_css_parser = { workspace = true }
biome_css_syntax = { workspace = true }
biome_diagnostics = { workspace = true }
biome_grit_parser = { workspace = true }
biome_grit_syntax = { workspace = true }
Expand Down
9 changes: 9 additions & 0 deletions crates/biome_grit_patterns/src/grit_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,3 +316,12 @@ pub struct GritTargetFile {
pub path: PathBuf,
pub parse: AnyParse,
}

impl GritTargetFile {
pub fn parse(source: &str, path: PathBuf, target_language: GritTargetLanguage) -> Self {
let parser = target_language.get_parser();
let parse = parser.parse_with_path(source, &path);

Self { parse, path }
}
}
76 changes: 76 additions & 0 deletions crates/biome_grit_patterns/src/grit_css_parser.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use crate::{
grit_analysis_ext::GritAnalysisExt, grit_target_language::GritTargetParser,
grit_tree::GritTargetTree,
};
use biome_css_parser::{parse_css, CssParserOptions};
use biome_css_syntax::CssLanguage;
use biome_parser::AnyParse;
use grit_util::{AnalysisLogs, FileOrigin, Parser, SnippetTree};
use std::path::Path;

pub struct GritCssParser;

impl GritTargetParser for GritCssParser {
fn from_cached_parse_result(
&self,
parse: &AnyParse,
path: Option<&Path>,
logs: &mut AnalysisLogs,
) -> Option<GritTargetTree> {
for diagnostic in parse.diagnostics() {
logs.push(diagnostic.to_log(path));
}

Some(GritTargetTree::new(parse.syntax::<CssLanguage>().into()))
}

fn parse_with_path(&self, source: &str, _path: &Path) -> AnyParse {
parse_css(source, CssParserOptions::default()).into()
}
}

impl Parser for GritCssParser {
type Tree = GritTargetTree;

fn parse_file(
&mut self,
body: &str,
path: Option<&Path>,
logs: &mut AnalysisLogs,
_old_tree: FileOrigin<'_, GritTargetTree>,
) -> Option<GritTargetTree> {
let parse_result = parse_css(body, CssParserOptions::default().allow_metavariables());

for diagnostic in parse_result.diagnostics() {
logs.push(diagnostic.to_log(path));
}

Some(GritTargetTree::new(parse_result.syntax().into()))
}

fn parse_snippet(
&mut self,
prefix: &'static str,
source: &str,
postfix: &'static str,
) -> SnippetTree<GritTargetTree> {
let context = format!("{prefix}{source}{postfix}");

let len = if cfg!(target_arch = "wasm32") {
|src: &str| src.chars().count() as u32
} else {
|src: &str| src.len() as u32
};

let parse_result = parse_css(&context, CssParserOptions::default().allow_metavariables());

SnippetTree {
tree: GritTargetTree::new(parse_result.syntax().into()),
source: source.to_owned(),
prefix,
postfix,
snippet_start: (len(prefix) + len(source) - len(source.trim_start())),
snippet_end: (len(prefix) + len(source.trim_end())),
}
}
}
16 changes: 14 additions & 2 deletions crates/biome_grit_patterns/src/grit_js_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::{
grit_tree::GritTargetTree,
};
use biome_js_parser::{parse, JsParserOptions};
use biome_js_syntax::JsFileSource;
use biome_js_syntax::{JsFileSource, JsLanguage};
use biome_parser::AnyParse;
use grit_util::{AnalysisLogs, FileOrigin, Parser, SnippetTree};
use std::path::Path;
Expand All @@ -21,7 +21,19 @@ impl GritTargetParser for GritJsParser {
logs.push(diagnostic.to_log(path));
}

Some(GritTargetTree::new(parse.syntax().into()))
Some(GritTargetTree::new(parse.syntax::<JsLanguage>().into()))
}

fn parse_with_path(&self, source: &str, path: &Path) -> AnyParse {
let source_type = match path.extension().and_then(|ext| ext.to_str()) {
Some("d.ts") => JsFileSource::d_ts(),
Some("js") => JsFileSource::js_module(),
Some("jsx") => JsFileSource::jsx(),
Some("tsx") => JsFileSource::tsx(),
_ => JsFileSource::ts(),
};

parse(source, source_type, JsParserOptions::default()).into()
}
}

Expand Down
11 changes: 8 additions & 3 deletions crates/biome_grit_patterns/src/grit_target_language.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
mod css_target_language;
mod js_target_language;

pub use css_target_language::CssTargetLanguage;
pub use js_target_language::JsTargetLanguage;

use crate::grit_css_parser::GritCssParser;
use crate::grit_js_parser::GritJsParser;
use crate::grit_target_node::{GritTargetNode, GritTargetSyntaxKind};
use crate::grit_tree::GritTargetTree;
Expand Down Expand Up @@ -147,13 +150,15 @@ macro_rules! generate_target_language {
}

generate_target_language! {
[CssTargetLanguage, GritCssParser],
[JsTargetLanguage, GritJsParser]
}

impl GritTargetLanguage {
/// Returns the target language to use for the given file extension.
pub fn from_extension(extension: &str) -> Option<Self> {
match extension {
"css" => Some(Self::CssTargetLanguage(CssTargetLanguage)),
"cjs" | "js" | "jsx" | "mjs" | "ts" | "tsx" => {
Some(Self::JsTargetLanguage(JsTargetLanguage))
}
Expand Down Expand Up @@ -196,9 +201,7 @@ impl GritTargetLanguage {
.tree
.root_node()
.descendants()
.map_or(false, |mut descendants| {
descendants.any(|descendant| descendant.kind().is_bogus())
});
.any(|descendant| descendant.kind().is_bogus());
if has_errors {
continue;
}
Expand Down Expand Up @@ -349,6 +352,8 @@ pub trait GritTargetParser: Parser<Tree = GritTargetTree> {
path: Option<&Path>,
logs: &mut AnalysisLogs,
) -> Option<GritTargetTree>;

fn parse_with_path(&self, source: &str, path: &Path) -> AnyParse;
}

#[derive(Clone, Debug)]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
mod constants;

use super::{
normalize_quoted_string, DisregardedSlotCondition, GritTargetLanguageImpl,
LeafEquivalenceClass, LeafNormalizer,
};
use crate::{
grit_target_node::{GritTargetNode, GritTargetSyntaxKind},
CompileError,
};
use biome_css_syntax::{CssLanguage, CssSyntaxKind};
use biome_rowan::{RawSyntaxKind, SyntaxKindSet};
use constants::DISREGARDED_SNIPPET_SLOTS;

const COMMENT_KINDS: SyntaxKindSet<CssLanguage> =
SyntaxKindSet::from_raw(RawSyntaxKind(CssSyntaxKind::COMMENT as u16)).union(
SyntaxKindSet::from_raw(RawSyntaxKind(CssSyntaxKind::MULTILINE_COMMENT as u16)),
);

const EQUIVALENT_LEAF_NODES: &[&[LeafNormalizer]] = &[&[LeafNormalizer::new(
GritTargetSyntaxKind::CssSyntaxKind(CssSyntaxKind::CSS_STRING_LITERAL),
normalize_quoted_string,
)]];

#[derive(Clone, Debug)]
pub struct CssTargetLanguage;

impl GritTargetLanguageImpl for CssTargetLanguage {
type Kind = CssSyntaxKind;

fn language_name(&self) -> &'static str {
"CSS"
}

/// Returns the syntax kind for a node by name.
///
/// 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<CssSyntaxKind> {
// TODO: See [super::JsTargetLanguage::kind_by_name()].
None
}

/// Returns the node name for a given syntax kind.
///
/// For compatibility with existing Grit snippets (as well as the online
/// Grit playground), node names should be aligned with TreeSitter's
/// `ts_language_symbol_name()`.
fn name_for_kind(&self, _kind: GritTargetSyntaxKind) -> &'static str {
// TODO: See [super::JsTargetLanguage::name_for_kind()].
"(unknown node)"
}

/// Returns the slots with their names for the given node kind.
///
/// 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 snippet_context_strings(&self) -> &[(&'static str, &'static str)] {
&[
("", ""),
("GRIT_BLOCK { ", " }"),
("GRIT_BLOCK { GRIT_PROPERTY: ", " }"),
]
}

fn is_comment_kind(kind: GritTargetSyntaxKind) -> bool {
kind.as_css_kind()
.map_or(false, |kind| COMMENT_KINDS.matches(kind))
}

fn metavariable_kind() -> Self::Kind {
CssSyntaxKind::CSS_METAVARIABLE
}

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,
text: &str,
) -> Result<Option<LeafEquivalenceClass>, CompileError> {
if let Some(class) = EQUIVALENT_LEAF_NODES
.iter()
.find(|v| v.iter().any(|normalizer| normalizer.kind() == kind))
{
LeafEquivalenceClass::new(text, kind, class)
} else {
Ok(None)
}
}
}

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

Loading

0 comments on commit ad8d7bd

Please sign in to comment.