Skip to content
Open
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
20 changes: 20 additions & 0 deletions .changeset/glimmer-template-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"@biomejs/biome": minor
---

Added support for Glimmer template files (`.gjs` and `.gts`). Biome can now parse, format, and lint Glimmer Component files used in Ember.js applications.

Glimmer templates are recognized using the `<template>...</template>` syntax and can appear in:
- Variable assignments: `const Tpl = <template>...</template>;`
- Class bodies: `class C { <template>...</template> }`
- Expression contexts
- Single unassigned templates (treated as default exports)

**Phase 1 Implementation Notes:**
- Template content is treated as **opaque tokens** - the content is preserved exactly as written without internal parsing or linting
- The template syntax itself is validated (e.g., checking for unclosed tags)
- Templates work with whitespace in the opening tag (e.g., `<template >`, `<template\n>`)
- LSP language IDs "gjs" and "gts" are now recognized
- Future phases will add internal template parsing and linting support

The template content is preserved as-is during formatting, and the parser provides diagnostics for unclosed template tags.
8 changes: 4 additions & 4 deletions crates/biome_grit_patterns/src/grit_js_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::{
grit_analysis_ext::GritAnalysisExt, grit_target_language::GritTargetParser,
grit_tree::GritTargetTree,
};
use biome_js_parser::{JsParserOptions, parse};
use biome_js_parser::{JsParserOptions, parse, parse_with_options};
use biome_js_syntax::{JsFileSource, JsLanguage};
use biome_parser::AnyParse;
use camino::Utf8Path;
Expand Down Expand Up @@ -34,7 +34,7 @@ impl GritTargetParser for GritJsParser {
_ => JsFileSource::ts(),
};

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

Expand All @@ -48,7 +48,7 @@ impl Parser for GritJsParser {
logs: &mut AnalysisLogs,
_old_tree: FileOrigin<'_, GritTargetTree>,
) -> Option<GritTargetTree> {
let parse_result = parse(
let parse_result = parse_with_options(
body,
JsFileSource::tsx(),
JsParserOptions::default().with_metavariables(),
Expand All @@ -75,7 +75,7 @@ impl Parser for GritJsParser {
|src: &str| src.len() as u32
};

let parse_result = parse(
let parse_result = parse_with_options(
&context,
JsFileSource::tsx(),
JsParserOptions::default().with_metavariables(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ pub fn kind_by_name(node_name: &str) -> Option<JsSyntaxKind> {
"JsFunctionExpression" => lang::JsFunctionExpression::KIND_SET.iter().next(),
"JsGetterClassMember" => lang::JsGetterClassMember::KIND_SET.iter().next(),
"JsGetterObjectMember" => lang::JsGetterObjectMember::KIND_SET.iter().next(),
"JsGlimmerTemplate" => lang::JsGlimmerTemplate::KIND_SET.iter().next(),
"JsIdentifierAssignment" => lang::JsIdentifierAssignment::KIND_SET.iter().next(),
"JsIdentifierBinding" => lang::JsIdentifierBinding::KIND_SET.iter().next(),
"JsIdentifierExpression" => lang::JsIdentifierExpression::KIND_SET.iter().next(),
Expand Down
2 changes: 1 addition & 1 deletion crates/biome_js_analyze/benches/js_analyzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ fn bench_analyzer(criterion: &mut Criterion) {
let file_source =
JsFileSource::try_from(test_case.path()).unwrap_or_default();
let parse =
biome_js_parser::parse(code, file_source, JsParserOptions::default());
biome_js_parser::parse_with_options(code, file_source, JsParserOptions::default());

let filter = AnalysisFilter {
categories: RuleCategoriesBuilder::default()
Expand Down
2 changes: 1 addition & 1 deletion crates/biome_js_analyze/src/ast_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ mod tests {
use super::get_boolean_value;

fn assert_boolean_value(code: &str, value: bool) {
let source = biome_js_parser::parse(code, JsFileSource::tsx(), JsParserOptions::default());
let source = biome_js_parser::parse_with_options(code, JsFileSource::tsx(), JsParserOptions::default());

if source.has_errors() {
panic!("syntax error")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ impl Rule for NoStaticOnlyClass {
AnyJsClassMember::JsBogusMember(_)
| AnyJsClassMember::JsMetavariable(_)
| AnyJsClassMember::JsEmptyClassMember(_) => None,
AnyJsClassMember::JsGlimmerTemplate(_) => Some(false), // Glimmer templates are non-static members (component content)
AnyJsClassMember::JsConstructorClassMember(_) => Some(false), // See GH#4482: Constructors are not regarded as static
AnyJsClassMember::TsConstructorSignatureClassMember(_) => Some(false), // See GH#4482: Constructors are not regarded as static
AnyJsClassMember::JsGetterClassMember(m) => Some(m.has_static_modifier()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ fn is_valid_constructor(expression: AnyJsExpression) -> Option<bool> {
| AnyJsExpression::JsBinaryExpression(_)
| AnyJsExpression::JsBogusExpression(_)
| AnyJsExpression::JsMetavariable(_)
| AnyJsExpression::JsGlimmerTemplate(_)
| AnyJsExpression::JsInstanceofExpression(_)
| AnyJsExpression::JsObjectExpression(_)
| AnyJsExpression::JsPostUpdateExpression(_)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ fn is_disallowed(expr: &AnyJsExpression) -> SyntaxResult<bool> {
| AnyJsExpression::JsComputedMemberExpression(_)
| AnyJsExpression::JsConditionalExpression(_)
| AnyJsExpression::JsFunctionExpression(_)
| AnyJsExpression::JsGlimmerTemplate(_)
| AnyJsExpression::JsIdentifierExpression(_)
| AnyJsExpression::JsImportMetaExpression(_)
| AnyJsExpression::JsInExpression(_)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1155,6 +1155,7 @@ fn selector_from_class_member(member: &AnyJsClassMember) -> Option<Selector> {
| AnyJsClassMember::JsConstructorClassMember(_)
| AnyJsClassMember::TsConstructorSignatureClassMember(_)
| AnyJsClassMember::JsEmptyClassMember(_)
| AnyJsClassMember::JsGlimmerTemplate(_)
| AnyJsClassMember::JsStaticInitializationBlockClassMember(_) => return None,
AnyJsClassMember::TsIndexSignatureClassMember(member) => {
(Kind::ClassProperty, (&member.modifiers()).into())
Expand Down
4 changes: 2 additions & 2 deletions crates/biome_js_analyze/src/react/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -666,11 +666,11 @@ impl ReactSuperClass {
#[cfg(test)]
mod test {
use super::*;
use biome_js_parser::{JsParserOptions, Parse, parse};
use biome_js_parser::{JsParserOptions, Parse, parse_with_options};
use biome_js_syntax::{AnyJsRoot, JsFileSource};

fn parse_jsx(code: &str) -> Parse<AnyJsRoot> {
let source = parse(code, JsFileSource::jsx(), JsParserOptions::default());
let source = parse_with_options(code, JsFileSource::jsx(), JsParserOptions::default());

if source.has_errors() {
panic!("syntax error")
Expand Down
8 changes: 4 additions & 4 deletions crates/biome_js_analyze/src/react/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ mod test {
#[test]
fn test_is_react_hook_call() {
{
let r = biome_js_parser::parse(
let r = biome_js_parser::parse_with_options(
r#"useRef();"#,
JsFileSource::js_module(),
JsParserOptions::default(),
Expand All @@ -352,7 +352,7 @@ mod test {
}

{
let r = biome_js_parser::parse(
let r = biome_js_parser::parse_with_options(
r#"userCredentials();"#,
JsFileSource::js_module(),
JsParserOptions::default(),
Expand All @@ -370,7 +370,7 @@ mod test {

#[test]
pub fn ok_react_stable_captures() {
let r = biome_js_parser::parse(
let r = biome_js_parser::parse_with_options(
r#"
import { useRef } from "react";
const ref = useRef();
Expand Down Expand Up @@ -400,7 +400,7 @@ mod test {

#[test]
pub fn ok_react_stable_captures_with_default_import() {
let r = biome_js_parser::parse(
let r = biome_js_parser::parse_with_options(
r#"
import * as React from "react";
const ref = React.useRef();
Expand Down
4 changes: 2 additions & 2 deletions crates/biome_js_analyze/src/services/semantic_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -924,7 +924,7 @@ fn is_used_in_expression_context(node: &AnyCandidateForUsedInExpressionNode) ->
#[cfg(test)]
mod tests {
use super::*;
use biome_js_parser::{JsParserOptions, Parse, parse};
use biome_js_parser::{JsParserOptions, Parse, parse_with_options};
use biome_js_syntax::{AnyJsRoot, JsFileSource, JsObjectBindingPattern};
use biome_rowan::AstNode;

Expand Down Expand Up @@ -984,7 +984,7 @@ mod tests {
}

fn parse_ts(code: &str) -> Parse<AnyJsRoot> {
let source = parse(code, JsFileSource::ts(), JsParserOptions::default());
let source = parse_with_options(code, JsFileSource::ts(), JsParserOptions::default());

if source.has_errors() {
panic!("syntax error")
Expand Down
40 changes: 20 additions & 20 deletions crates/biome_js_analyze/src/suppressions.tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use super::*;
use biome_analyze::{AnalyzerOptions, Never, RuleCategoriesBuilder, RuleFilter};
use biome_diagnostics::category;
use biome_diagnostics::{Diagnostic, DiagnosticExt, Severity, print_diagnostic_to_string};
use biome_js_parser::{JsParserOptions, parse};
use biome_js_parser::{JsParserOptions, parse_with_options};
use biome_js_syntax::{JsFileSource, TextRange, TextSize};
use biome_package::{Dependencies, PackageJson};
use std::slice;
Expand All @@ -12,7 +12,7 @@ use std::slice;
fn quick_test() {
const SOURCE: &str = r#"f({ prop: () => {} })"#;

let parsed = parse(SOURCE, JsFileSource::tsx(), JsParserOptions::default());
let parsed = parse_with_options(SOURCE, JsFileSource::tsx(), JsParserOptions::default());

let mut error_ranges: Vec<TextRange> = Vec::new();
let options = AnalyzerOptions::default();
Expand Down Expand Up @@ -70,7 +70,7 @@ fn quick_test_suppression() {
}
";

let parsed = parse(
let parsed = parse_with_options(
SOURCE,
JsFileSource::js_module(),
JsParserOptions::default(),
Expand Down Expand Up @@ -152,7 +152,7 @@ fn suppression() {
}
";

let parsed = parse(
let parsed = parse_with_options(
SOURCE,
JsFileSource::js_module(),
JsParserOptions::default(),
Expand Down Expand Up @@ -221,7 +221,7 @@ fn suppression_syntax() {
a == b;
";

let parsed = parse(
let parsed = parse_with_options(
SOURCE,
JsFileSource::js_module(),
JsParserOptions::default(),
Expand Down Expand Up @@ -264,7 +264,7 @@ let foo = 2;
let bar = 33;
";

let parsed = parse(
let parsed = parse_with_options(
SOURCE,
JsFileSource::js_module(),
JsParserOptions::default(),
Expand Down Expand Up @@ -315,7 +315,7 @@ let bar = 33;
debugger;
";

let parsed = parse(
let parsed = parse_with_options(
SOURCE,
JsFileSource::js_module(),
JsParserOptions::default(),
Expand Down Expand Up @@ -361,7 +361,7 @@ let bar = 33;
debugger;
";

let parsed = parse(
let parsed = parse_with_options(
SOURCE,
JsFileSource::js_module(),
JsParserOptions::default(),
Expand Down Expand Up @@ -409,7 +409,7 @@ let bar = 33;
debugger;
";

let parsed = parse(
let parsed = parse_with_options(
SOURCE,
JsFileSource::js_module(),
JsParserOptions::default(),
Expand Down Expand Up @@ -458,7 +458,7 @@ let foo = 2;
let bar = 33;
";

let parsed = parse(
let parsed = parse_with_options(
SOURCE,
JsFileSource::js_module(),
JsParserOptions::default(),
Expand Down Expand Up @@ -505,7 +505,7 @@ let foo = 2;
let bar = 33;
";

let parsed = parse(
let parsed = parse_with_options(
SOURCE,
JsFileSource::js_module(),
JsParserOptions::default(),
Expand Down Expand Up @@ -550,7 +550,7 @@ let foo = 2;
let bar = 33;
";

let parsed = parse(
let parsed = parse_with_options(
SOURCE,
JsFileSource::js_module(),
JsParserOptions::default(),
Expand Down Expand Up @@ -598,7 +598,7 @@ let foo = 2;
let bar = 33;
";

let parsed = parse(
let parsed = parse_with_options(
SOURCE,
JsFileSource::js_module(),
JsParserOptions::default(),
Expand Down Expand Up @@ -645,7 +645,7 @@ let c;
// biome-ignore-end lint/style/useConst: single rule
";

let parsed = parse(
let parsed = parse_with_options(
SOURCE,
JsFileSource::js_module(),
JsParserOptions::default(),
Expand Down Expand Up @@ -695,7 +695,7 @@ debugger;

";

let parsed = parse(
let parsed = parse_with_options(
SOURCE,
JsFileSource::js_module(),
JsParserOptions::default(),
Expand Down Expand Up @@ -748,7 +748,7 @@ let d;

";

let parsed = parse(
let parsed = parse_with_options(
SOURCE,
JsFileSource::js_module(),
JsParserOptions::default(),
Expand Down Expand Up @@ -794,7 +794,7 @@ const foo0 = function (bar: string) {
bar = 'baz';
};";

let parsed = parse(SOURCE, JsFileSource::ts(), JsParserOptions::default());
let parsed = parse_with_options(SOURCE, JsFileSource::ts(), JsParserOptions::default());

let enabled_rules = vec![
RuleFilter::Rule("complexity", "useArrowFunction"),
Expand Down Expand Up @@ -840,7 +840,7 @@ a == b;

";

let parsed = parse(
let parsed = parse_with_options(
SOURCE,
JsFileSource::js_module(),
JsParserOptions::default(),
Expand Down Expand Up @@ -884,7 +884,7 @@ var foo = {
}
"#;

let parsed = parse(
let parsed = parse_with_options(
SOURCE,
JsFileSource::js_module(),
JsParserOptions::default(),
Expand Down Expand Up @@ -926,7 +926,7 @@ fn suppression_issue_5562() {
// biome-ignore lint/suspicious/noConsole: foo
console.log("should be suppressed");"#;

let parsed = parse(
let parsed = parse_with_options(
SOURCE,
JsFileSource::js_module(),
JsParserOptions::default(),
Expand Down
Loading