DEV Community

Herrington Darkholme
Herrington Darkholme

Posted on

Biome's GritQL Plugin vs. ast-grep: Your Guide to AST-Based Code Transformation for JS/TS Devs

Comparison

Introduction – Why AST Tools Matter for Native Linters

Maintaining consistent, high-quality code in large projects presents a significant challenge. While modern Rust-based linting tools deliver impressive performance for enforcing basic coding standards, they frequently fall short when developers require highly custom, project-specific patterns or automated, large-scale refactors across a codebase. This is where AST-based tools, particularly with the development of plugin systems in native linters, become essential.

In this report, we will delve into two prominent AST-based tools: Biome's new GritQL plugin and the established ast-grep. We will compare their syntax, performance, features, and more, providing a comprehensive guide to help developers choose the most suitable tool for their specific needs.

Meet the Contenders: A Quick Overview

Before diving into a head-to-head comparison, let's get acquainted with each tool individually, understanding their core functionalities and design philosophies.

Biome's GritQL Plugin: The New Kid on the Biome Block

Biome is a fast formatter and linter designed for web projects, intended to serve as a comprehensive replacement for tools like Prettier and ESLint. With its 2.0 beta release, Biome introduced a plugin system, initially supporting GritQL for defining custom lint rules.

The plugin operates by utilizing .grit files, which contain patterns written in GritQL. GritQL itself is a specialized query language designed for searching and transforming syntax trees. These .grit files are used to define custom diagnostic rules that Biome can then apply to a codebase. Configuration is straightforward: developers enable the plugin by adding the path to their .grit file within the plugins array in their biome.json configuration file.

As of the Biome 2.0 beta, the GritQL plugin is currently diagnostic-only. This means it excels at identifying and reporting specific code patterns, but it does not yet offer the capability to automatically fix them. However, the implementation of fixable rules, which will leverage GritQL's rewrite operator (=>), is a planned feature, indicating an ongoing development towards more comprehensive capabilities.

ast-grep: The Established AST Powerhouse

ast-grep stands as a mature and robust command-line tool, built on the high-performance Rust programming language. Its operational mechanism involves defining rules for searching, linting, and rewriting code using YAML files. ast-grep leverages tree-sitter, a powerful parsing library, to construct its Abstract Syntax Trees, enabling deep code understanding.

A significant advantage of ast-grep is its comprehensive set of capabilities, offering full fix and rewrite functionalities directly out of the box. This makes it a versatile tool for automated code modifications, ranging from simple linting autofixes to complex large-scale refactors. Its reliance on tree-sitter also provides extensive language support beyond just JavaScript, TypeScript, JSX, TSX, HTML, and CSS. It supports a wide array of programming languages including Python, Java, Go, Rust, and etc, making it a truly polyglot tool.

Biome's GritQL plugin extends Biome's existing linter primarily for custom diagnostics, whereas ast-grep is a foundational and versatile AST manipulation tool with a broader scope, including robust rewriting capabilities.

Head-to-Head: A Deep Dive into Comparison

Now that we've had a quick introduction to both tools, let's put them side-by-side and explore their differences across key aspects that matter to developers.

Syntax Showdown: How Rules Are Written

Understanding how to write rules is fundamental to using any AST-based tool. Let's examine equivalent examples in both Biome's GritQL plugin and ast-grep to compare their pattern languages and rule definition approaches.

Example 1: Detect console.log() Statements

Our goal here is to find all instances where console.log(...) is used, typically to identify debugging statements.

Biome GritQL Plugin (detect-console-log.grit):

For more detailed setup instructions, please kindly refer to the Isco's article.

language js (typescript, jsx);

// Pattern to match any call to console.log and register a warning
`console.log($$$arguments)` where {
  register_diagnostic(
    span = $$$arguments, // Highlight the arguments of the call
    message = "Avoid using console.log. Consider a dedicated logger or removing it for production.",
    severity = "warn"
  )
}
Enter fullscreen mode Exit fullscreen mode

To enable this rule, add the path to your .grit file in your biome.json configuration:

{
    "linter": {
        "enabled": true,
        "rules": {
            "all": true
        }
    },
    "plugins": [ "./detect-console-log.grit"]
}
Enter fullscreen mode Exit fullscreen mode

Explanation of Biome GritQL Rule:

This GritQL rule precisely targets console.log calls.

  1. language js (typescript, jsx);: Sets the parser for JavaScript, TypeScript, and JSX.
  2. `console.log($$$arguments)`: This is the literal code pattern. $$$arguments is a spread metavariable that matches any number of arguments passed to console.log().
  3. where { register_diagnostic(...) }: When the pattern matches, register_diagnostic() is called to report an issue. It highlights the $$$arguments part of the code, provides a custom message, and sets the severity to "warn".

ast-grep (rules/no-console-log.yml):

For more detailed setup instructions, please kindly refer to makotot's article or the official site.

# rules/no-console-log.yml
id: no-console-log
language: TypeScript # or JavaScript
severity: warn
message: "Avoid using console.log. Consider a dedicated logger or removing it for production."
rule:
    pattern: console.log($$$ARGS)
    # fix: "" # Uncomment to automatically remove the console.log call
Enter fullscreen mode Exit fullscreen mode

Explanation of ast-grep Rule:

This ast-grep rule similarly detects console.log statements using a declarative YAML structure.

  1. id, language, severity, message: Standard fields providing rule identification, target language, warning level, and a descriptive message.
  2. rule: pattern: console.log($$$ARGS): This defines the code pattern to match. $$$ARGS is a spread metavariable that captures any number of arguments to console.log().
  3. # fix: "": A commented-out fix field demonstrates ast-grep's auto-rewriting capability; uncommenting it would remove the matched console.log() statement.

Syntax Takeaways for Example 1:

  • Pattern Matching: Both tools use a similar, intuitive pattern syntax (console.log($$$args)) with spread metavariables to match function calls irrespective of their arguments.
  • Diagnostic Reporting: GritQL uses a register_diagnostic() function within a where clause, while ast-grep uses direct YAML fields (message, severity) for rule metadata.
  • Rewrite Capability: ast-grep includes an explicit fix field for automated code transformations, a feature still planned for GritQL's direct rewrite operator.

Example 2: Detect React Components in createFileRoute Files

This rule aims to enforce a common architectural pattern in frameworks like TanStack Router: route files should primarily define routes and their data loaders, not React UI components. We want to flag any React component definitions (function or arrow function, named or default export) within files that import createFileRoute from @tanstack/react-router. This demonstrates a more complex, multi-step rule involving checking for an import and then searching for specific patterns conditionally.

Biome GritQL Plugin (no-components-in-route-file.grit):

Code snippet

// First check if this file imports createFileRoute from @tanstack/react-router
file(body=$program) where {
  // Check for the specific import within the entire file's AST
  $program <: contains bubble `import { $imports } from "@tanstack/react-router"` where {
      $imports <: contains `createFileRoute` // Ensure 'createFileRoute' is among the imported identifiers
  },
  // If the import exists, proceed to find component definitions anywhere within the file
  $program <: contains bubble or {
    // 1. Function declaration components (e.g., function MyComponent() { ... })
    `function $ComponentName($props) {
      $body
    }` where {
      $ComponentName <: r"^[A-Z][a-zA-Z0-9]*$", // Regex constraint: ensures it's a PascalCase identifier
      register_diagnostic(span=$ComponentName, message=`Component should not be defined directly in a route file. Move it to a separate UI component file.`, severity="error")
    },
    // 2. Arrow function components assigned to a variable (e.g., const MyComponent = () => { ... })
    `const $ComponentName = ($props) => {
      $body
    }` where {
      $ComponentName <: r"^[A-Z][a-zA-Z0-9]*$", // Regex constraint: ensures it's a PascalCase identifier
      register_diagnostic(span=$ComponentName, message=`Component should not be defined directly in a route file. Move it to a separate UI component file.`, severity="error")
    },
    // 3. Default exported function components (e.g., export default function MyComponent() { ... })
    `export default function $ComponentName($props) {
      $body
    }` where {
      register_diagnostic(span=$ComponentName, message="Default exported function components should not be defined in route files. Move it to a separate UI component file.", severity="error")
    },
    // 4. Default exported anonymous arrow function components (e.g., export default () => { ... })
    `export default ($props) => {
      $body
    }` where {
      register_diagnostic(span=$props, message="Default exported arrow functions should not be defined in route files. Move it to a separate UI component file.", severity="error")
    },
    // 5. Named exported function components (e.g., export function MyComponent() { ... })
    `export function $ComponentName($props) {
      $body
    }` where {
      $ComponentName <: r"^[A-Z][a-zA-Z0-9]*$",
      register_diagnostic(span=$ComponentName, message=`Exported component should not be defined directly in a route file. Move it to a separate UI component file.`, severity="error")
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Explanation of Biome GritQL Rule:

This GritQL rule uses a powerful combination of top-level file matching, recursive searching (contains with bubble), and logical grouping (or and where) to implement the complex architectural check.

  1. file(body=$program) where { ... }:

    • file(): This is a GritQL function that signifies the entire source file being analyzed. It's the highest-level entry point for pattern matching, ensuring the rule considers the full context of the code.
    • body=$program: This captures the entire Abstract Syntax Tree (AST) of the file into a metavariable named $program. This $program metavariable effectively becomes the root node for all subsequent searches and conditions within this rule.
    • where { ... }: This clause introduces conditions that must all be met for the file() pattern to be considered a match and for any diagnostics within to be registered.
  2. $program <: contains bubble \import { $imports } from "@tanstack/react-router"where { $imports <: contains \createFileRoute}:

    • This is the first primary condition within the file()'s where clause. Its purpose is to check if the current file contains the specific import createFileRoute from @tanstack/react-router.
    • $program <:: This syntax indicates that $program (the entire file's AST) must contain or match the pattern that follows.
    • contains: This is a GritQL predicate that performs a search for the specified pattern within the given AST node ($program in this case). It only searches the immediate children of program. This is crucial for excluding elements that are not be at the top level of the file.
    • bubble: When used with contains, the bubble clause creates a new, isolated scope for metavariables defined within the pattern it's applied to. In this specific pattern (`import { $imports } ...`), the metavariable is $imports. By using bubble, the $imports metavariable and its value are confined to this particular contains clause. This ensures that:
      • If there were other metavariables named $imports elsewhere in the rule, their values would not conflict with the $imports captured by this specific import statement match.
      • The subsequent where { $imports <: containscreateFileRoute} clause can directly refer to the $imports captured by this specific import pattern and apply a further condition to it, without ambiguity. It allows for modular reasoning about nested matches.
    • `import { $imports } from "@tanstack/react-router"`: This is the literal code pattern GritQL searches for. The $imports metavariable captures the destructured part of the import statement (e.g., createFileRoute, createLoader).
    • where { $imports <: containscreateFileRoute}: This is a nested condition applied specifically to the $imports metavariable captured by the import pattern. It uses contains again to verify that the string createFileRoute is present within the identifiers captured by $imports. This ensures we are only targeting files specifically using createFileRoute from the router library.
  3. $program <: contains bubble or { ... }:

    • This is the second primary condition in the file()'s where clause. This condition is only evaluated if the first condition (the import check) is met. Its purpose is to find any React component definitions within the file.
    • contains bubble: Similar to the first condition, this performs a recursive search throughout the entire file's AST ($program) for any of the patterns defined within the or block. The bubble again ensures that any metavariables (like $ComponentName, $props, $body) captured within each individual component pattern are scoped locally to that specific pattern match. This means, for example, if there's a function MyComponent and later const MyOtherComponent, the $ComponentName metavariable will correctly capture "MyComponent" for the first match and "MyOtherComponent" for the second, without conflict.
    • or { ... }: This is a logical OR operator. It means that if any of the patterns listed inside its curly braces match, this entire contains condition is satisfied. This is essential for detecting the various ways a React component can be defined (function declaration, arrow function, default export, named export).
    • Individual Component Patterns (e.g., `function $ComponentName($props) { $body }`): Each block defines a common React component structure using GritQL's pattern syntax.
      • $ComponentName, $props, $body: These are metavariables capturing the component's name, props, and body respectively.
      • $ComponentName <: r"^[A-Z][a-zA-Z0-9]*$": This applies a regular expression predicate to the $ComponentName metavariable, ensuring the captured component name follows the PascalCase convention typical for React components (starts with an uppercase letter, followed by alphanumeric characters).
      • register_diagnostic(span=$ComponentName, message=..., severity="error"): This is the action that occurs when a component pattern successfully matches and all preceding where conditions (including the createFileRoute import check) are satisfied.
        • span=$ComponentName: Directs Biome to highlight the AST node corresponding to the component's name when reporting the issue.
        • message: The informative error message displayed to the developer.
        • severity: Sets the diagnostic level (e.g., "error", "warn").

In Summary for GritQL: The rule operates by first establishing that the file is a "route file" (by finding the createFileRoute import). Only if that condition holds true does it then proceed to recursively scan the same file for any common React component definitions. The bubble clause is critical for isolating the scope of metavariables within the contains patterns, ensuring that complex, multi-part rules can be built without unintended variable collisions or ambiguities across different matched sub-patterns.


ast-grep (rules/no-components-in-route-file.yml):

YAML

# rules/no-components-in-route-file.yml
id: no-components-in-route-file
language: TypeScript # or JavaScript, JSX, TSX
severity: error
message: "React components should not be defined directly in files that import createFileRoute. Move them to a separate UI component file."
rule:
  kind: program
  all: # Both conditions must be true for the rule to apply
  - has: # Condition 1: Check for the createFileRoute import
      pattern: import $$$IMP from "@tanstack/react-router"
      regex: createFileRoute # Ensure 'createFileRoute' is part of the imported identifiers
  - has: # Condition 2: Check for the presence of a React component pattern
      any: # Match any of these component definitions
      - pattern: function $COMPONENT($$$ARGS) { $$$ } # Function declaration
      - pattern: const $COMPONENT = ($$$ARGS) => $$$ # Arrow function assigned to a variable
      - pattern: export default ($$$PROPS) => $$$ # Default exported anonymous arrow function
      - pattern: export default function $COMPONENT($$$ARGS) { $$$ } # Default exported function
      - pattern: export function $COMPONENT($$$ARGS) { $$$ } # Named exported function
constraints: # Ensure PascalCase naming convention
  COMPONENT: { regex: "^[A-Z][a-zA-Z0-9]*" } 
Enter fullscreen mode Exit fullscreen mode

Explanation of ast-grep Rule:

This ast-grep rule utilizes YAML's declarative structure with kind, all, has, and any to define the same complex logical checks.

  1. id, language, severity, message: These are standard metadata fields for an ast-grep rule, providing a unique identifier for the rule, specifying the programming language(s) it applies to, defining its diagnostic severity level (e.g., "error"), and providing the human-readable message that will be displayed when the rule is triggered.

  2. rule: kind: program:

    • kind: program: This specifies the target AST node type that the rule should be applied to. program typically represents the entire source file or compilation unit. This ensures the rule examines the complete code context, similar to GritQL's file().
  3. all: [...]: This is a logical AND operator in ast-grep. It means that all of the sub-rules listed within its array must successfully match for the overall rule to be considered true. In this example, it enforces that both the import condition and the component presence condition must be met simultaneously.

  4. First sub-rule: - has: pattern: import $$$IMP from "@tanstack/react-router" and regex: createFileRoute:

    • This is the first condition under the all clause. It is designed to verify the presence of the createFileRoute import statement.
    • has: This ast-grep rule field indicates that the current node (program in this context) must contain (as a descendant anywhere in its subtree) the specified pattern.
    • pattern: import $$$IMP from "@tanstack/react-router": This is the structural pattern for the import statement. $$$IMP is a metavariable that captures the entire list of destructured identifiers within the curly braces (e.g., { createFileRoute, anotherImport }).
    • regex: createFileRoute: This field applies a regular expression specifically to the text captured by the $$$IMP metavariable. It ensures that the exact string createFileRoute is present within that captured text.
  5. Second sub-rule: - has: any: [...]:

    • This is the second condition under the all clause. It is responsible for detecting various forms of React component definitions. This condition is only evaluated if the first condition (the createFileRoute import check) has passed.
    • has: Similar to the first has rule, this performs a recursive search within the program node for any of the patterns defined within the nested any block.
    • any: [...]: This is a logical OR operator in ast-grep. If any of the pattern rules listed in this array match within the program's AST, this has condition is satisfied. This flexibility allows the rule to catch all common ways React components are structured.
    • Individual Component Patterns (e.g., - pattern: function $COMPONENT($$$ARGS) { $$$ }): Each item in the any array defines a specific structural pattern for a React component.
      • $COMPONENT, $$$ARGS, $$$ (wildcard for body): These are ast-grep's metavariables. $COMPONENT captures a single AST node (like a function name), $$$ARGS captures a sequence of nodes (like function arguments), and $$$ is a generic wildcard that matches any sequence of nodes (often used for a function body or statement block).
  6. constraints: constraints field adds a check that $COMPONENT meta variable passes a PascalCase validation on component names.

In Summary for ast-grep: The rule is structured to apply to the entire file. It then uses an "AND" (all) condition to combine two primary "contains" (has) checks: one to confirm the presence of the createFileRoute import (with a regex to verify the specific identifier), and another to confirm the presence of any common React component patterns. If both checks pass, the rule triggers, reporting the architectural violation.

Syntax Takeaways for Example 2:

  • Complex Conditional Logic: This example highlights how both tools can implement rules requiring multiple conditions. GritQL uses chained where clauses with contains and bubble for deep searches and or for multiple alternatives. ast-grep uses all to combine necessary conditions and has with nested any to match a variety of component patterns.
  • Targeted Matching: Both tools can precisely target specific code constructs (imports, function declarations, arrow functions) and even apply additional constraints (like the PascalCase regex in GritQL for named components, which would require an additional regex or matches sub-rule in ast-grep for full parity).
  • Rule Scope: Both file(body=$program) in GritQL and kind: program in ast-grep indicate that the rule operates on the entire file's Abstract Syntax Tree.

Performance: Speed Matters

Performance is a critical factor for any code analysis tool, especially those integrated into linters that run frequently during development or within continuous integration/continuous deployment (CI/CD) pipelines. After all, linting speed is why we migrate from the more customizable eslint.

Real-World Case Study:

A real-world case study, documented in GitHub issue biomejs/biome#6210, highlighted a significant performance discrepancy between the two tools. In this scenario, a user created a custom Biome GritQL plugin rule designed to enforce specific conventions related to component definitions in files importing createFileRoute from @tanstack/react-router.

  • Original Biome GritQL rule performance: The initial version of this custom rule took a staggering 70 seconds to execute on the VS Code repository.
  • Biome performance without the rule: For context, Biome's base performance without this custom rule was remarkably fast, completing the same task in less than 2 seconds. This stark contrast clearly indicated that the custom GritQL rule was the primary bottleneck.
  • Equivalent ast-grep rule performance: An equivalent rule implemented in ast-grep performed significantly better, completing the same task on the same repository in a mere 0.5 seconds.
  • Optimized Biome GritQL rule performance: The Biome team and GritQL developers identified that the performance issue stemmed from unoptimized GritQL patterns. After optimization, which involved using more specific patterns like file(body=$program) and bubble, the Biome GritQL rule showed substantial improvement, adding only about 1 second of overhead to Biome's base performance.

Key Performance Takeaways:

  • ast-grep's general high performance: ast-grep consistently demonstrates superior performance. Its Rust core and optimized AST traversal mechanisms contribute to its speed, offering reliable and impressive performance across various scenarios.
  • Potential performance pitfalls with Biome's GritQL plugin (beta): As a newer tool still in beta, the initial implementation or the use of certain broad GritQL patterns (such as $program and contains without careful scoping) can lead to significant slowdowns. This indicates that while the tool is powerful, its current state requires careful consideration in rule design.
  • Importance of optimized patterns: The case study vividly illustrates that the way a GritQL rule is authored can drastically impact its performance within Biome. The Biome team and GritQL developers are actively aware of these considerations and are working on further optimizations and providing guidance for writing efficient patterns.

Conclusion on Performance: ast-grep currently offers superior and more consistent performance, especially for complex rules. Biome's GritQL plugin, being newer and in beta, shows promise but currently requires careful pattern authoring to avoid performance bottlenecks. However, performance is expected to improve as Biome and its GritQL integration continue to mature.

The stark contrast in initial performance (70 seconds for an unoptimized Biome GritQL rule versus 0.5 seconds for ast-grep) followed by Biome's optimization to 1 second overhead reveals a critical distinction. ast-grep delivers high performance as an inherent feature, largely out-of-the-box, due to its Rust core and optimized traversal. For Biome's GritQL plugin, however, achieving optimal performance is currently a challenge that the rule author must actively manage through careful pattern design and optimization. This means that the learning curve for Biome's GritQL isn't just about understanding its syntax, but also about mastering its performance characteristics and best practices for writing efficient rules. This can add a significant burden to developers, especially for complex or frequently run rules, potentially negating some of the benefits of integrating within the Biome ecosystem if not managed effectively.

Documentation & Learning Curve: Getting Started

The quality and accessibility of documentation, along with the overall learning curve, significantly impact a developer's ability to adopt and effectively use a new tool.

Biome GritQL Plugin Documentation:
Biome's official website provides documentation for its plugin system, covering how to enable plugins and the basic usage of the register_diagnostic() function. However, for the intricate details of the GritQL syntax itself—the language used to write the patterns—users need to refer to a separate official GritQL documentation, which is hosted on docs.grit.io.

Biome GritQL Plugin Learning Curve:
The learning curve for Biome's GritQL Plugin involves understanding two distinct systems. While Biome's plugin system is relatively straightforward to grasp, the GritQL language itself is more complex. GritQL has its own unique syntax and concepts, including patterns, predicates, where clauses, and specific built-in functions like contains and bubble. Furthermore, as demonstrated in the performance section, the need to write performant GritQL rules adds another layer of complexity to the learning curve. Currently, there isn't an interactive playground specifically integrated for Biome's GritQL plugin within the Biome ecosystem, although Grit.io does offer a general interactive playground for the GritQL language.

ast-grep Documentation:
ast-grep boasts comprehensive documentation available directly on its website, ast-grep.github.io. This includes detailed guides on its pattern syntax, rule configuration using YAML, advanced features, and API usage. A significant advantage for learning and testing patterns is the availability of an excellent interactive playground directly on its website (ast-grep.github.io/playground.html), which is invaluable for experimentation and rapid prototyping of rules.

ast-grep Learning Curve:
The YAML-based rule configuration of ast-grep is generally considered straightforward for developers to pick up. Its pattern syntax, which employs $META_VAR for single AST nodes and $$$VAR for sequences of nodes, is described as relatively intuitive for developers already familiar with code manipulation concepts. While understanding AST node kinds (derived from tree-sitter) can be beneficial for crafting more advanced rules, it is not strictly necessary for many common use cases. The tool also offers powerful relational (e.g., has, inside) and composite (e.g., all, any, not) rule fields, which may require some time to master for complex scenarios but provide significant flexibility.

Conclusion on Documentation & Learning:
ast-grep currently has a more mature and integrated documentation ecosystem, significantly enhanced by its valuable interactive playground. Its YAML and pattern syntax might feel more immediately accessible to some developers due to their more conventional structure. GritQL, while powerful, has a distinct syntax that necessitates dedicated learning. The fragmented documentation experience for Biome's plugin—requiring users to refer to Biome's site for plugin integration and Grit.io for the GritQL language itself—can make the initial learning journey slightly more fragmented and less cohesive.

The "split documentation" for Biome's GritQL plugin and the absence of a dedicated, integrated playground (requiring users to navigate to Grit.io for general GritQL experimentation) introduce unnecessary friction. Learning a new Domain Specific Language (DSL) like GritQL is already a cognitive hurdle; having to jump between disparate documentation sites and lacking integrated interactive tools exacerbates this challenge. In contrast, ast-grep's unified documentation and interactive playground streamline the entire learning process, making it much more approachable and efficient. This difference directly impacts how quickly a developer can become productive in writing custom rules, suggesting that a cohesive and interactive learning environment is a strong competitive advantage. Biome's current fragmented approach, though understandable for a beta product, poses a higher initial barrier to entry for new users.

Editor Support

Seamless integration with Integrated Development Environments (IDEs) and text editors is crucial for developer workflow and productivity.

Biome GritQL Plugin:
Biome's existing LSP integrations mean GritQL plugin diagnostics appear directly in editors like VSCode, JetBrains IDEs, and Neovim, just like other lint rules. However, dedicated editor support for authoring .grit files is limited, with only a VSCode extension available. Its LSP server currently struggles with wider adoption as other editors don't yet universally recognize the .grit file type for advanced authoring features.

ast-grep:

ast-grep naturally supports a wider range of editors for rule authoring because the file format is YAML. Diagnostics and code actions are available in any LSP-compliant editor:

  • VSCode: An official extension provides a rich Structural Search & Replace UI, diagnostics, Code Actions (quick fixes), and schema validation for rule.yml files, greatly streamlining rule creation.
  • Neovim: Support exists through nvim-lspconfig for its LSP server, complemented by specialized plugins like coc-ast-grep, telescope-sg, and grug-far.nvim.
  • LSP Server: Its standalone Language Server Protocol (LSP) server (ast-grep lsp) ensures seamless integration and broad compatibility for diagnostics and code actions in any LSP-compliant editor.

Conclusion on Editor Support:
ast-grep provides a more mature and comprehensive editor experience, particularly for authoring custom rules, with its dedicated VSCode UI, schema validation, and broad LSP compatibility. In contrast, Biome's GritQL plugin's rule authoring experience is currently more basic, constrained by limited editor-specific tooling and wider LSP recognition of .grit files. This makes ast-grep a more productive choice for frequent rule development.

Key Features at a Glance

This table provides a concise, side-by-side summary of the most important features and characteristics discussed throughout this comparison. For busy developers, a quick glance at this table allows for rapid assessment of the core differences and an initial evaluation of which tool aligns best with their immediate needs. It serves as a quick reference guide, reinforcing the detailed points made in the preceding sections and making the comparison digestible and actionable.

Aspect Biome GritQL Plugin (v2.0 beta) ast-grep
Core Functionality Custom diagnostics for Biome linter AST-based search, lint, and rewrite
Syntax Language GritQL YAML for rules, custom pattern syntax
Fix/Rewrite Capability No (Planned Feature) Yes (Built-in fix field in rules)
Performance (General) Variable (can be slow if not optimized) Generally very high (Rust-based, multi-threading)
Documentation Quality Biome docs + GritQL docs (separate) Comprehensive, integrated ast-grep docs
Debugging Rudimentary Official interactive playground
Learning Curve Moderate to High (GritQL syntax + perf considerations) Low to Moderate (YAML + intuitive patterns)
Editor Support Diagnostics displayed via Biome's LSP Rich VSCode extension, Neovim plugins, general LSP server
Standalone CLI Availability No (part of Biome CLI) Yes (sg command)

5. Choosing Your Champion: When to Pick Which Tool

The decision between Biome's GritQL plugin and ast-grep is not about one tool being universally "better," but rather about aligning with specific project needs, existing toolchains, and tolerance for beta features. Each tool has distinct strengths that make it more suitable for certain scenarios.

Choose Biome's GritQL Plugin if:

  • Existing Biome Investment: A development team is already heavily invested in the Biome ecosystem and seeks to extend its linting capabilities with custom rules without introducing another standalone tool.
  • Custom Linting Diagnostics: The primary need is to add custom diagnostic rules to Biome's linter specifically for JavaScript/TypeScript or CSS, and immediate requirements for automated fixes are not a top priority.
  • GritQL Comfort: The developers are comfortable with the GritQL syntax or are willing to learn its distinct approach and its nuances regarding performance optimization.
  • Acceptance of Beta Status and Future Fixes: The team is comfortable with the current beta status of the plugin and its present lack of autofixes, and is actively monitoring for this planned feature to be implemented in the future.
  • Optimized Performance Alignment: The performance characteristics, once rules are carefully optimized, align with the workflow's requirements for linting speed.

Choose ast-grep if:

  • Immediate Search and Rewrite Capabilities: Powerful, performant AST-based search and rewrite capabilities are needed right now, including robust autofixing and complex codemods.
  • Mature and Stable Tool: A mature, stable tool with a rich feature set and strong editor support (including a dedicated VSCode UI for structural search/replace and robust assistance in writing rules) is preferred.
  • Standalone CLI Tool: A standalone CLI tool is needed for scripting, performing one-off refactoring tasks, or integrating into CI pipelines independently of a specific linter.
  • YAML Preference: Developers prefer YAML for rule configuration and find its pattern syntax more immediately accessible and intuitive.
  • Complex Codemods and Autofixes: The goal is to build complex codemods or enforce coding standards with automated fixes, which is a core strength of ast-grep.
  • Interactive Playground: An interactive playground for developing and testing rules is an important part of the development workflow.

The decision between Biome's GritQL plugin and ast-grep is about aligning with specific project needs, existing toolchains, and tolerance for beta features. Biome's plugin is tightly integrated and serves as an enhancement for existing Biome users, simplifying custom linting within their established ecosystem. Conversely, ast-grep functions as a powerful, versatile standalone solution for broader AST manipulation tasks, including complex refactoring and multi-language support. This distinction means that if a team's primary goal is to extend Biome's linting, the plugin offers convenience. However, if the requirements extend to automated code transformations, polyglot analysis, or flexible CLI integration, ast-grep becomes the more capable and immediate choice. Developers should carefully evaluate their primary use case (e.g., custom linting within an existing Biome setup versus general AST search/rewrite/codemods), their existing development ecosystem, and their comfort level with beta features and dedicated learning curves to make the most informed decision.

6. Final Thoughts

Both Biome's GritQL plugin and ast-grep offer powerful AST-based code manipulation capabilities, moving beyond traditional text-based tools for custom standards, refactoring, and quality.

For immediate, robust AST manipulation with comprehensive fixing, broad language support, and strong editor integration, ast-grep is the clear and versatile choice. Biome's GritQL plugin, though in beta and currently diagnostic-only, is a promising addition for existing Biome users seeking integrated custom linting, with planned autofix features and performance optimizations on the horizon.

Ultimately, selecting between them depends on specific project needs: ast-grep for immediate, versatile AST control, or Biome's GritQL for extending an existing Biome setup and embracing its evolving ecosystem. The ongoing innovation from both integrated and standalone AST tooling promises an exciting future for code analysis and transformation.

Top comments (0)