diff --git a/Cargo.lock b/Cargo.lock index 2962ff5ec9bc..128711ab3561 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -933,6 +933,8 @@ dependencies = [ "biome_flags", "biome_formatter", "biome_fs", + "biome_graphql_parser", + "biome_graphql_syntax", "biome_grit_patterns", "biome_js_analyze", "biome_js_factory", diff --git a/crates/biome_formatter/src/lib.rs b/crates/biome_formatter/src/lib.rs index 63a8296046c5..73ea11439350 100644 --- a/crates/biome_formatter/src/lib.rs +++ b/crates/biome_formatter/src/lib.rs @@ -44,6 +44,7 @@ mod verbatim; use crate::formatter::Formatter; use crate::group_id::UniqueGroupIdBuilder; use crate::prelude::TagKind; +use std::fmt; use std::fmt::{Debug, Display}; use crate::builders::syntax_token_cow_slice; @@ -673,7 +674,7 @@ impl FormatContext for SimpleFormatContext { } } -#[derive(Debug, Default, Eq, PartialEq)] +#[derive(Debug, Default, Eq, PartialEq, Copy, Clone)] pub struct SimpleFormatOptions { pub indent_style: IndentStyle, pub indent_width: IndentWidth, @@ -713,6 +714,12 @@ impl FormatOptions for SimpleFormatOptions { } } +impl Display for SimpleFormatOptions { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fmt::Debug::fmt(self, f) + } +} + /// Lightweight sourcemap marker between source and output tokens #[derive(Debug, Copy, Clone, Eq, PartialEq)] #[cfg_attr( diff --git a/crates/biome_graphql_syntax/Cargo.toml b/crates/biome_graphql_syntax/Cargo.toml index eb560cd3bf0d..2799185c66a1 100644 --- a/crates/biome_graphql_syntax/Cargo.toml +++ b/crates/biome_graphql_syntax/Cargo.toml @@ -15,10 +15,10 @@ version = "0.1.0" [dependencies] biome_rowan = { workspace = true } schemars = { workspace = true, optional = true } -serde = { workspace = true, features = ["derive"], optional = true } +serde = { workspace = true, features = ["derive"] } [features] -serde = ["dep:serde", "schemars", "biome_rowan/serde"] +schema = ["schemars", "biome_rowan/serde"] [lints] workspace = true diff --git a/crates/biome_graphql_syntax/src/file_source.rs b/crates/biome_graphql_syntax/src/file_source.rs new file mode 100644 index 000000000000..fa26d3df7750 --- /dev/null +++ b/crates/biome_graphql_syntax/src/file_source.rs @@ -0,0 +1,66 @@ +use biome_rowan::FileSourceError; +use std::ffi::OsStr; +use std::path::Path; + +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[derive( + Debug, Clone, Default, Copy, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize, +)] +pub struct GraphqlFileSource {} + +impl GraphqlFileSource { + /// Try to return the GraphQL file source corresponding to this file name from well-known files + pub fn try_from_well_known(file_name: &str) -> Result { + // TODO: to be implemented + Err(FileSourceError::UnknownFileName(file_name.into())) + } + + /// Try to return the GraphQL file source corresponding to this file extension + pub fn try_from_extension(extension: &str) -> Result { + match extension { + "graphql" | "gql" => Ok(Self::default()), + _ => Err(FileSourceError::UnknownExtension( + Default::default(), + extension.into(), + )), + } + } + + /// Try to return the GraphQL file source corresponding to this language ID + /// + /// See the [LSP spec] and [VS Code spec] for a list of language identifiers + /// + /// [LSP spec]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem + /// [VS Code spec]: https://code.visualstudio.com/docs/languages/identifiers + pub fn try_from_language_id(language_id: &str) -> Result { + match language_id { + "graphql" => Ok(Self::default()), + _ => Err(FileSourceError::UnknownLanguageId(language_id.into())), + } + } +} + +impl TryFrom<&Path> for GraphqlFileSource { + type Error = FileSourceError; + + fn try_from(path: &Path) -> Result { + let file_name = path + .file_name() + .and_then(OsStr::to_str) + .ok_or_else(|| FileSourceError::MissingFileName(path.into()))?; + + if let Ok(file_source) = Self::try_from_well_known(file_name) { + return Ok(file_source); + } + + // We assume the file extensions are case-insensitive + // and we use the lowercase form of them for pattern matching + let extension = &path + .extension() + .and_then(OsStr::to_str) + .map(str::to_lowercase) + .ok_or_else(|| FileSourceError::MissingFileExtension(path.into()))?; + + Self::try_from_extension(extension) + } +} diff --git a/crates/biome_graphql_syntax/src/lib.rs b/crates/biome_graphql_syntax/src/lib.rs index 5f70663173a6..6a568ecca334 100644 --- a/crates/biome_graphql_syntax/src/lib.rs +++ b/crates/biome_graphql_syntax/src/lib.rs @@ -4,10 +4,12 @@ #[macro_use] mod generated; +mod file_source; mod syntax_node; use biome_rowan::{AstNode, RawSyntaxKind, SyntaxKind}; pub use biome_rowan::{TextLen, TextRange, TextSize, TokenAtOffset, TriviaPieceKind, WalkEvent}; +pub use file_source::GraphqlFileSource; pub use generated::*; pub use syntax_node::*; diff --git a/crates/biome_grit_parser/Cargo.toml b/crates/biome_grit_parser/Cargo.toml index e577f5672d92..43b2f5cbdcfb 100644 --- a/crates/biome_grit_parser/Cargo.toml +++ b/crates/biome_grit_parser/Cargo.toml @@ -31,9 +31,7 @@ quickcheck_macros = { workspace = true } tests_macros = { workspace = true } [features] -schemars = ["dep:schemars"] -serde = ["biome_grit_syntax/serde"] -tests = [] +schemars = ["dep:schemars", "biome_grit_syntax/schema"] # cargo-workspaces metadata [package.metadata.workspaces] diff --git a/crates/biome_grit_syntax/Cargo.toml b/crates/biome_grit_syntax/Cargo.toml index c1075a446d27..bcfe37dce43f 100644 --- a/crates/biome_grit_syntax/Cargo.toml +++ b/crates/biome_grit_syntax/Cargo.toml @@ -13,10 +13,10 @@ version = "0.5.7" [dependencies] biome_rowan = { workspace = true } schemars = { workspace = true, optional = true } -serde = { workspace = true, features = ["derive"], optional = true } +serde = { workspace = true, features = ["derive"] } [features] -serde = ["dep:serde", "schemars", "biome_rowan/serde"] +schema = ["schemars", "biome_rowan/serde"] [lints] workspace = true diff --git a/crates/biome_service/Cargo.toml b/crates/biome_service/Cargo.toml index ce16489c8839..0135841a35c5 100644 --- a/crates/biome_service/Cargo.toml +++ b/crates/biome_service/Cargo.toml @@ -27,6 +27,8 @@ biome_diagnostics = { workspace = true } biome_flags = { workspace = true } biome_formatter = { workspace = true, features = ["serde"] } biome_fs = { workspace = true, features = ["serde"] } +biome_graphql_parser = { workspace = true } +biome_graphql_syntax = { workspace = true } biome_grit_patterns = { workspace = true } biome_js_analyze = { workspace = true } biome_js_factory = { workspace = true, optional = true } @@ -65,8 +67,11 @@ schema = [ "biome_text_edit/schemars", "biome_json_syntax/schema", "biome_css_syntax/schema", + "biome_graphql_syntax/schema", ] +graphql = [] + [dev-dependencies] insta = { workspace = true } tests_macros = { workspace = true } diff --git a/crates/biome_service/src/file_handlers/graphql.rs b/crates/biome_service/src/file_handlers/graphql.rs new file mode 100644 index 000000000000..d1c82f4327ad --- /dev/null +++ b/crates/biome_service/src/file_handlers/graphql.rs @@ -0,0 +1,112 @@ +use super::{DocumentFileSource, ExtensionHandler, ParseResult}; +use crate::file_handlers::DebugCapabilities; +use crate::file_handlers::{ + AnalyzerCapabilities, Capabilities, FormatterCapabilities, ParserCapabilities, +}; +use crate::settings::{ + FormatSettings, LanguageListSettings, LanguageSettings, LinterSettings, OverrideSettings, + ServiceLanguage, Settings, +}; +use crate::workspace::GetSyntaxTreeResult; +use biome_analyze::{AnalyzerConfiguration, AnalyzerOptions}; +use biome_formatter::SimpleFormatOptions; +use biome_fs::BiomePath; +use biome_graphql_parser::parse_graphql_with_cache; +use biome_graphql_syntax::{GraphqlLanguage, GraphqlRoot, GraphqlSyntaxNode}; +use biome_parser::AnyParse; +use biome_rowan::NodeCache; + +impl ServiceLanguage for GraphqlLanguage { + type FormatterSettings = (); + type LinterSettings = (); + type OrganizeImportsSettings = (); + type FormatOptions = SimpleFormatOptions; + type ParserSettings = (); + type EnvironmentSettings = (); + + fn lookup_settings(language: &LanguageListSettings) -> &LanguageSettings { + &language.graphql + } + + fn resolve_format_options( + _global: Option<&FormatSettings>, + _overrides: Option<&OverrideSettings>, + _language: Option<&()>, + _path: &BiomePath, + _document_file_source: &DocumentFileSource, + ) -> Self::FormatOptions { + SimpleFormatOptions::default() + } + + fn resolve_analyzer_options( + _global: Option<&Settings>, + _linter: Option<&LinterSettings>, + _overrides: Option<&OverrideSettings>, + _language: Option<&Self::LinterSettings>, + path: &BiomePath, + _file_source: &DocumentFileSource, + ) -> AnalyzerOptions { + AnalyzerOptions { + configuration: AnalyzerConfiguration::default(), + file_path: path.to_path_buf(), + } + } +} + +#[derive(Debug, Default, PartialEq, Eq)] +pub(crate) struct GraphqlFileHandler; + +impl ExtensionHandler for GraphqlFileHandler { + fn capabilities(&self) -> Capabilities { + Capabilities { + parser: ParserCapabilities { parse: Some(parse) }, + debug: DebugCapabilities { + debug_syntax_tree: Some(debug_syntax_tree), + debug_control_flow: None, + debug_formatter_ir: None, + }, + analyzer: AnalyzerCapabilities { + lint: None, + code_actions: None, + rename: None, + fix_all: None, + organize_imports: None, + }, + formatter: FormatterCapabilities { + format: None, + format_range: None, + format_on_type: None, + }, + } + } +} + +fn parse( + _biome_path: &BiomePath, + file_source: DocumentFileSource, + text: &str, + _settings: Option<&Settings>, + cache: &mut NodeCache, +) -> ParseResult { + let parse = parse_graphql_with_cache(text, cache); + let root = parse.syntax(); + let diagnostics = parse.into_diagnostics(); + + ParseResult { + any_parse: AnyParse::new( + // SAFETY: the parser should always return a root node + root.as_send().unwrap(), + diagnostics, + ), + language: Some(file_source), + } +} + +fn debug_syntax_tree(_rome_path: &BiomePath, parse: AnyParse) -> GetSyntaxTreeResult { + let syntax: GraphqlSyntaxNode = parse.syntax(); + let tree: GraphqlRoot = parse.tree(); + GetSyntaxTreeResult { + cst: format!("{syntax:#?}"), + ast: format!("{tree:#?}"), + } +} diff --git a/crates/biome_service/src/file_handlers/mod.rs b/crates/biome_service/src/file_handlers/mod.rs index f705e03e5c64..b18ac63ad81a 100644 --- a/crates/biome_service/src/file_handlers/mod.rs +++ b/crates/biome_service/src/file_handlers/mod.rs @@ -3,6 +3,7 @@ use self::{ unknown::UnknownFileHandler, }; pub use crate::file_handlers::astro::{AstroFileHandler, ASTRO_FENCE}; +use crate::file_handlers::graphql::GraphqlFileHandler; pub use crate::file_handlers::svelte::{SvelteFileHandler, SVELTE_FENCE}; pub use crate::file_handlers::vue::{VueFileHandler, VUE_FENCE}; use crate::settings::Settings; @@ -21,6 +22,7 @@ use biome_css_syntax::CssFileSource; use biome_diagnostics::{Diagnostic, Severity}; use biome_formatter::Printed; use biome_fs::BiomePath; +use biome_graphql_syntax::GraphqlFileSource; use biome_js_parser::{parse, JsParserOptions}; use biome_js_syntax::{EmbeddingKind, JsFileSource, Language, TextRange, TextSize}; use biome_json_syntax::JsonFileSource; @@ -33,6 +35,7 @@ use std::path::Path; mod astro; mod css; +mod graphql; mod javascript; mod json; mod svelte; @@ -47,6 +50,7 @@ pub enum DocumentFileSource { Js(JsFileSource), Json(JsonFileSource), Css(CssFileSource), + Graphql(GraphqlFileSource), #[default] Unknown, } @@ -69,6 +73,12 @@ impl From for DocumentFileSource { } } +impl From for DocumentFileSource { + fn from(value: GraphqlFileSource) -> Self { + Self::Graphql(value) + } +} + impl From<&Path> for DocumentFileSource { fn from(path: &Path) -> Self { Self::from_path(path) @@ -86,6 +96,11 @@ impl DocumentFileSource { if let Ok(file_source) = CssFileSource::try_from_well_known(file_name) { return Ok(file_source.into()); } + if cfg!(feature = "graphql") { + if let Ok(file_source) = GraphqlFileSource::try_from_well_known(file_name) { + return Ok(file_source.into()); + } + } Err(FileSourceError::UnknownFileName(file_name.into())) } @@ -105,6 +120,11 @@ impl DocumentFileSource { if let Ok(file_source) = CssFileSource::try_from_extension(extension) { return Ok(file_source.into()); } + if cfg!(feature = "graphql") { + if let Ok(file_source) = GraphqlFileSource::try_from_extension(extension) { + return Ok(file_source.into()); + } + } Err(FileSourceError::UnknownExtension( Default::default(), extension.into(), @@ -127,6 +147,11 @@ impl DocumentFileSource { if let Ok(file_source) = CssFileSource::try_from_language_id(language_id) { return Ok(file_source.into()); } + if cfg!(feature = "graphql") { + if let Ok(file_source) = GraphqlFileSource::try_from_language_id(language_id) { + return Ok(file_source.into()); + } + } Err(FileSourceError::UnknownLanguageId(language_id.into())) } @@ -248,6 +273,13 @@ impl DocumentFileSource { } } + pub fn to_graphql_file_source(&self) -> Option { + match self { + DocumentFileSource::Graphql(graphql) => Some(*graphql), + _ => None, + } + } + pub fn to_css_file_source(&self) -> Option { match self { DocumentFileSource::Css(css) => Some(*css), @@ -265,6 +297,9 @@ impl DocumentFileSource { EmbeddingKind::None => true, }, DocumentFileSource::Json(_) | DocumentFileSource::Css(_) => true, + DocumentFileSource::Graphql(_) => { + cfg!(feature = "graphql") + } DocumentFileSource::Unknown => false, } } @@ -295,6 +330,7 @@ impl biome_console::fmt::Display for DocumentFileSource { } } DocumentFileSource::Css(_) => fmt.write_markup(markup! { "CSS" }), + DocumentFileSource::Graphql(_) => fmt.write_markup(markup! { "GraphQL" }), DocumentFileSource::Unknown => fmt.write_markup(markup! { "Unknown" }), } } @@ -453,6 +489,7 @@ pub(crate) struct Features { vue: VueFileHandler, svelte: SvelteFileHandler, unknown: UnknownFileHandler, + graphql: GraphqlFileHandler, } impl Features { @@ -464,6 +501,7 @@ impl Features { astro: AstroFileHandler {}, vue: VueFileHandler {}, svelte: SvelteFileHandler {}, + graphql: GraphqlFileHandler {}, unknown: UnknownFileHandler::default(), } } @@ -483,6 +521,7 @@ impl Features { }, DocumentFileSource::Json(_) => self.json.capabilities(), DocumentFileSource::Css(_) => self.css.capabilities(), + DocumentFileSource::Graphql(_) => self.graphql.capabilities(), DocumentFileSource::Unknown => self.unknown.capabilities(), } } diff --git a/crates/biome_service/src/settings.rs b/crates/biome_service/src/settings.rs index d9b4497a9cde..c1595369818b 100644 --- a/crates/biome_service/src/settings.rs +++ b/crates/biome_service/src/settings.rs @@ -18,6 +18,7 @@ use biome_deserialize::{Merge, StringSet}; use biome_diagnostics::Category; use biome_formatter::{AttributePosition, IndentStyle, IndentWidth, LineEnding, LineWidth}; use biome_fs::BiomePath; +use biome_graphql_syntax::GraphqlLanguage; use biome_js_analyze::metadata; use biome_js_formatter::context::JsFormatOptions; use biome_js_parser::JsParserOptions; @@ -439,6 +440,7 @@ pub struct LanguageListSettings { pub javascript: LanguageSettings, pub json: LanguageSettings, pub css: LanguageSettings, + pub graphql: LanguageSettings, } impl From for LanguageSettings { diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 2b40cd4086e8..ec97458a5aa4 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -2154,7 +2154,8 @@ export type DocumentFileSource = | "Unknown" | { Js: JsFileSource } | { Json: JsonFileSource } - | { Css: CssFileSource }; + | { Css: CssFileSource } + | { Graphql: GraphqlFileSource }; export interface JsFileSource { /** * Used to mark if the source is being used for an Astro, Svelte or Vue file @@ -2172,6 +2173,7 @@ export interface JsonFileSource { export interface CssFileSource { variant: CssVariant; } +export interface GraphqlFileSource {} export type EmbeddingKind = "Astro" | "Vue" | "Svelte" | "None"; export type Language = | "JavaScript" diff --git a/xtask/rules_check/src/lib.rs b/xtask/rules_check/src/lib.rs index 1d26f87f844b..c3efb7ca6b82 100644 --- a/xtask/rules_check/src/lib.rs +++ b/xtask/rules_check/src/lib.rs @@ -378,7 +378,7 @@ fn assert_lint( } } // Unknown code blocks should be ignored by tests - DocumentFileSource::Unknown => {} + DocumentFileSource::Unknown | DocumentFileSource::Graphql(_) => {} } if test.expect_diagnostic {