Skip to content

GritQL plugins: expose leading-trivia / JSDoc-comment matching predicate #10474

@brewpirate

Description

@brewpirate

Use case

Writing a GritQL plugin that flags exported declarations missing a preceding /** … */ block — equivalent to ESLint's eslint-plugin-tsdoc syntax check plus a presence check that doesn't yet exist there either.

The presence check is the load-bearing piece: "does this exported function/class/interface/type/etc. have a /** */ block attached to it?" This is straightforwardly expressible with the TypeScript compiler API (ts.getJSDocCommentsAndTags(node)) and the internal biome_jsdoc_comment crate has the same primitive (JsdocComment::get_jsdocs(node)), but neither is reachable from a GritQL plugin.

What I tried empirically (biome v2.4)

Pattern Behavior
$fn <: not after '/** $_ */' Fires on documented AND undocumented functions. after/before match sibling AST nodes, not leading-trivia comments.
$fn <: not preceded_by '/** $_ */' Silently no-op. preceded_by isn't a registered predicate; biome fails open rather than erroring.
'/** $doc */ export function ...' (positive doc-capture) Silently no-op. The comment isn't part of the declaration's text in the CST that GritQL matches against.
not $fn <: contains '/** $_ */' Fires on both. contains checks the function body, not its surroundings.

So biome v2.4's GritQL surface cannot inspect leading comments — either via a positive /** */ capture pattern or via a negative-predicate. Trivia is unreachable from the plugin layer.

What works internally — but not in plugins

crates/biome_jsdoc_comment/src/jsdoc_comment.rs has:

pub fn get_jsdocs(node: &JsSyntaxNode) -> impl Iterator<Item = String> {
    node.first_token()
        .into_iter()
        .flat_map(|token| token.leading_trivia().pieces())
        .filter_map(|trivia| {
            let text = trivia.text();
            matches!(
                trivia.kind(),
                TriviaPieceKind::SingleLineComment | TriviaPieceKind::MultiLineComment
            )
            .then(|| text)
            .filter(|text: &&str| Self::text_is_jsdoc_comment(text))
            .map(|text| text.to_owned())
        })
}

Used by biome's own Rust-built rules and the formatter — but not exposed to GritQL plugins.

Proposal

One of:

Option A — new GritQL predicate with_leading_jsdoc

`export function $name($_): $_ { $_ }` as $fn where {
  $fn <: not with_leading_jsdoc()
}

Boolean predicate. Returns true if the node has at least one /** */ leading trivia piece via get_jsdocs(node).next().is_some().

Option B — expose leading_comments as a matchable list

`export function $name($_): $_ { $_ }` as $fn where {
  not ($fn.leading_comments contains `/** $_ */`)
}

More general; lets plugins also match //-style comments above declarations (useful for documenting that single-line comments should be promoted to TSDoc — a sibling concern).

Option C — positive doc-capture pattern actually works

`/** $doc */ export function $name($_): $_ { $_ }` as $matched

Today this silently no-ops. If biome's GritQL pattern matcher concatenated leading trivia into the matchable text span, this would work and feels more in the spirit of GritQL's "pattern is the source" model.

Why I'm filing this

Working around the gap means projects implement a sibling TS-compiler-API scanner per repo, duplicating logic biome already has internally. Concrete example: https://github.com/brewpirate/helios/blob/main/scripts/detect-tsdoc.ts — ~400 LOC TypeScript using ts.getJSDocCommentsAndTags to do what a 5-line GritQL plugin should be able to express.

Happy to contribute any of these options if there's appetite for direction.

Related

  • biome useTsdoc lint rule (not yet shipped per Biome 2.4)
  • microsoft/tsdoc eslint-plugin (validates content of existing JSDoc; doesn't solve presence)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions