mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 18:43:44 +00:00
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:
1 parent
bd37e78d2a
commit
a5d8184f92
4 files changed
+118
-1
No files matched your search
+8
@@ -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
|
||||
}
|
||||
+25
@@ -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 {}
|
||||
+26
@@ -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 {}
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user