fix(jsdoc): parse code spans after quotes in descriptions (#27955)

Fixes #27852, closes #27925.

- Recognize Markdown code spans after quotes in JSDoc descriptions, so a span wrapping onto the next line does not swallow subsequent tags.
- Preserve quoted backticks inside types, optional parameter defaults, parenthesized function arguments, and `@import` specifiers.
- Add parser regressions and formatter input/expected-output fixtures for single and double quotes.
This commit is contained in:
leaysgur committed 2026-10-19 07:06:03 +00:00
1 parent bd37e78d2a
commit a5d8184f92
4 files changed
+118 -1

No files matched your search

@@ -0,0 +1,8 @@
{
"single_quote": true,
"comment_line_strategy": "multiline",
"description_tag": true,
"description_with_dot": true,
"prefer_code_fences": true,
"separate_tag_groups": false
}
@@ -0,0 +1,25 @@
/**
* @description Summary.
*
* @remarks
* A row's. `{ a, b }`.
* @example
* ```ts
* const a = 1;
* ```
*
* @see {@link Foo}
*/
export class X {}
/**
* @remarks
* A row"s. `a, b`.
* @example
* ```ts
* const a = 1;
* ```
*
* @see {@link Foo}
*/
export class Y {}
@@ -0,0 +1,26 @@
/**
* Summary.
*
* @remarks
* A row's. `{ a,
* b }`.
* @example
* ```ts
* const a = 1;
* ```
*
* @see {@link Foo}
*/
export class X {}
/**
* @remarks
* A row"s. `a,
* b`.
* @example
* ```ts
* const a = 1;
* ```
* @see {@link Foo}
*/
export class Y {}
+59 -1
View File
@@ -44,6 +44,7 @@ pub fn parse_jsdoc(
// This includes package names when doing a @import
let mut in_double_quotes = false;
let mut in_single_quotes = false;
let mut in_import_tag = false;
// Tracks whether we're at the logical start of a line.
// Only `@` at the start of a line (after optional whitespace and `*` markers)
@@ -85,7 +86,12 @@ pub fn parse_jsdoc(
// - 2 backticks: inline code (used to escape backticks inside)
// - 3+ backticks: code fence (for nested code blocks)
// Opening and closing sequences must have the same number of backticks.
'`' if !in_single_quotes && !in_double_quotes => {
'`' if (!in_single_quotes && !in_double_quotes)
|| (curly_brace_depth == 0
&& square_brace_depth == 0
&& brace_depth == 0
&& !in_import_tag) =>
{
// Count consecutive backticks
let mut count: u32 = 1;
while chars.peek() == Some(&'`') {
@@ -162,6 +168,9 @@ pub fn parse_jsdoc(
}
start = end;
in_import_tag = source_text[start..]
.strip_prefix("@import")
.is_some_and(|rest| rest.starts_with(char::is_whitespace));
}
_ => {}
}
@@ -275,6 +284,55 @@ mod tests {
assert_eq!(kinds, vec!["param", "returns"]);
}
#[test]
fn quotes_before_code_spans_do_not_prevent_tag_split() {
for description in [
"A row's. `a,\n * b`.",
"A row\"s. `a,\n * b`.",
"A row's. `{ a,\n * b }`.",
"A row\"s. `{ a,\n * b }`.",
"A row's. ``a,\n * b``.",
"A 'row'. `a,\n * b`.",
"A row's. `a, b`.",
"A `a,\n * b`. row's.",
"A rows. `a,\n * b`.",
] {
let source = format!(
"\n * @remarks\n * {description}\n * @example\n * ```ts\n * const a = 1;\n * ```\n * @see {{@link Foo}}"
);
assert_eq!(tag_kinds(&source), vec!["remarks", "example", "see"], "{description}");
}
}
#[test]
fn backticks_inside_import_specifiers_do_not_prevent_tag_split() {
for import in [
"@import { Foo } from './`foo'",
"@import { Foo } from \"./`foo\"",
"@import\n * { Foo } from './`foo'",
] {
let source =
format!("\n * {import}\n * @remarks A row's. `a,\n * b`.\n * @see {{@link Foo}}");
assert_eq!(tag_kinds(&source), vec!["import", "remarks", "see"], "{import}");
}
}
#[test]
fn backticks_inside_optional_param_defaults_do_not_prevent_tag_split() {
for parameter in ["[name='`']", "[name=\"`\"]"] {
let source = format!("\n * @param {{string}} {parameter}\n * @returns {{string}}");
assert_eq!(tag_kinds(&source), vec!["param", "returns"], "{parameter}");
}
}
#[test]
fn backticks_inside_function_arguments_do_not_prevent_tag_split() {
for example in ["console.log('`');", "console.log(\"`\");"] {
let source = format!("\n * @example\n * {example}\n * @returns {{void}}");
assert_eq!(tag_kinds(&source), vec!["example", "returns"], "{example}");
}
}
#[test]
fn extra_closing_brace_does_not_prevent_tag_split() {
// Bug B: extra `}` after param name decremented curly_brace_depth