feat(semantic/jsdoc): Add Span for JSDoc, JSDocTag (#2815)

This commit is contained in:
Yuji Sugiura 2024-03-26 19:40:31 +09:00 committed by GitHub
parent c5ccd5e7a1
commit df744b205a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 318 additions and 106 deletions

View file

@ -156,7 +156,8 @@ impl<'a> JSDocBuilder<'a> {
}
// Remove the very first `*`
Some(JSDoc::new(&comment_content[1..]))
let jsdoc_span = Span::new(comment_span.start + 1, comment_span.end);
Some(JSDoc::new(&comment_content[1..], jsdoc_span))
}
}

View file

@ -1,29 +1,124 @@
use super::jsdoc_tag::JSDocTag;
use super::parse::parse_jsdoc;
use oxc_span::Span;
use std::cell::OnceCell;
#[derive(Debug, Clone)]
pub struct JSDoc<'a> {
raw: &'a str,
/// Cached+parsed JSDoc comment and tags
cached: OnceCell<(String, Vec<JSDocTag<'a>>)>,
cached: OnceCell<(String, Vec<(Span, JSDocTag<'a>)>)>,
pub span: Span,
}
impl<'a> JSDoc<'a> {
/// comment_content: Inside of /**HERE*/, not include `/**` and `*/`
pub fn new(comment_content: &'a str) -> JSDoc<'a> {
Self { raw: comment_content, cached: OnceCell::new() }
/// span: `Span` for this JSDoc comment, range for `/**HERE*/`
pub fn new(comment_content: &'a str, span: Span) -> JSDoc<'a> {
Self { raw: comment_content, cached: OnceCell::new(), span }
}
pub fn comment(&self) -> &str {
&self.parse().0
}
pub fn tags(&self) -> &Vec<JSDocTag<'a>> {
pub fn tags(&self) -> &Vec<(Span, JSDocTag<'a>)> {
&self.parse().1
}
fn parse(&self) -> &(String, Vec<JSDocTag<'a>>) {
self.cached.get_or_init(|| parse_jsdoc(self.raw))
fn parse(&self) -> &(String, Vec<(Span, JSDocTag<'a>)>) {
self.cached.get_or_init(|| parse_jsdoc(self.raw, self.span.start))
}
}
#[cfg(test)]
mod test {
use crate::{Semantic, SemanticBuilder};
use oxc_allocator::Allocator;
use oxc_parser::Parser;
use oxc_span::SourceType;
fn build_semantic<'a>(
allocator: &'a Allocator,
source_text: &'a str,
source_type: Option<SourceType>,
) -> Semantic<'a> {
let source_type = source_type.unwrap_or_default();
let ret = Parser::new(allocator, source_text, source_type).parse();
let program = allocator.alloc(ret.program);
let semantic = SemanticBuilder::new(source_text, source_type)
.with_trivias(ret.trivias)
.build(program)
.semantic;
semantic
}
#[test]
fn get_jsdoc_span() {
let allocator = Allocator::default();
let semantic = build_semantic(
&allocator,
r"
/** single line */
/**
* multi
* line
*/
/**
multi
line
*/
",
Some(SourceType::default()),
);
let mut jsdocs = semantic.jsdoc().iter_all();
let jsdoc = jsdocs.next().unwrap();
assert_eq!(jsdoc.span.source_text(semantic.source_text), " single line ");
let jsdoc = jsdocs.next().unwrap();
assert_eq!(
jsdoc.span.source_text(semantic.source_text),
"\n * multi\n * line\n "
);
let jsdoc = jsdocs.next().unwrap();
assert_eq!(jsdoc.span.source_text(semantic.source_text), "\nmulti\nline\n ");
}
#[test]
fn get_jsdoc_tag_span() {
let allocator = Allocator::default();
let semantic = build_semantic(
&allocator,
r"
/** single line @k1 d1 */
/**
* multi
* line
* @k2 d2
* d2
* @k3 d3
* @k4 d4
* d4
*/
",
Some(SourceType::default()),
);
let mut jsdocs = semantic.jsdoc().iter_all();
let jsdoc = jsdocs.next().unwrap();
let mut tags = jsdoc.tags().iter();
let (span, _) = tags.next().unwrap();
assert_eq!(span.source_text(semantic.source_text), "@k1");
let jsdoc = jsdocs.next().unwrap();
let mut tags = jsdoc.tags().iter();
let (span, _) = tags.next().unwrap();
assert_eq!(span.source_text(semantic.source_text), "@k2");
let (span, _) = tags.next().unwrap();
assert_eq!(span.source_text(semantic.source_text), "@k3");
let (span, _) = tags.next().unwrap();
assert_eq!(span.source_text(semantic.source_text), "@k4");
}
}

View file

@ -36,7 +36,7 @@ pub struct JSDocTag<'a> {
impl<'a> JSDocTag<'a> {
/// kind: Does not contain the `@` prefix
/// raw_body: The body part of the tag, after the `@kind {HERE_MAY_BE_MULTILINE...}`
/// raw_body: The body part of the tag, after the `@kind{HERE_MAY_BE_MULTILINE...}`
pub fn new(kind: &'a str, raw_body: &'a str) -> JSDocTag<'a> {
debug_assert!(!kind.starts_with('@'));
Self { raw_body, kind }
@ -51,7 +51,7 @@ impl<'a> JSDocTag<'a> {
/// @kind
/// ```
pub fn comment(&self) -> String {
utils::trim_multiline_comment(self.raw_body)
utils::trim_comment(self.raw_body)
}
/// Use for `@type`, `@satisfies`, ...etc.
@ -85,7 +85,7 @@ impl<'a> JSDocTag<'a> {
None => (None, self.raw_body),
};
(type_part, utils::trim_multiline_comment(comment_part))
(type_part, utils::trim_comment(comment_part))
}
/// Use for `@param`, `@property`, `@typedef`, ...etc.
@ -119,7 +119,7 @@ impl<'a> JSDocTag<'a> {
None => (None, ""),
};
(type_part, name_part, utils::trim_multiline_comment(comment_part))
(type_part, name_part, utils::trim_comment(comment_part))
}
}
@ -130,43 +130,43 @@ mod test {
#[test]
fn parses_comment() {
assert_eq!(JSDocTag::new("a", "").comment(), "");
assert_eq!(JSDocTag::new("a", "c1").comment(), "c1");
assert_eq!(JSDocTag::new("a", " c2 \n z ").comment(), "c2\nz");
assert_eq!(JSDocTag::new("a", "* c3\n * \n z \n\n").comment(), "c3\nz");
assert_eq!(JSDocTag::new("a", " c1").comment(), "c1");
assert_eq!(JSDocTag::new("a", "\nc2 \n z ").comment(), "c2\nz");
assert_eq!(JSDocTag::new("a", "\n* c3\n * \n z \n\n").comment(), "c3\nz");
assert_eq!(
JSDocTag::new("a", "comment4 and {@inline tag}!").comment(),
JSDocTag::new("a", " comment4 and {@inline tag}!").comment(),
"comment4 and {@inline tag}!"
);
}
#[test]
fn parses_type() {
assert_eq!(JSDocTag::new("t", "{t1}").r#type(), Some("t1"));
assert_eq!(JSDocTag::new("t", "{t2} foo").r#type(), Some("t2"));
assert_eq!(JSDocTag::new("t", " {t1}").r#type(), Some("t1"));
assert_eq!(JSDocTag::new("t", "\n{t2} foo").r#type(), Some("t2"));
assert_eq!(JSDocTag::new("t", " {t3 } ").r#type(), Some("t3 "));
assert_eq!(JSDocTag::new("t", " ").r#type(), None);
assert_eq!(JSDocTag::new("t", "t4").r#type(), None);
assert_eq!(JSDocTag::new("t", "{t5 ").r#type(), None);
assert_eq!(JSDocTag::new("t", "{t6}\nx").r#type(), Some("t6"));
assert_eq!(JSDocTag::new("t", " t4").r#type(), None);
assert_eq!(JSDocTag::new("t", " {t5 ").r#type(), None);
assert_eq!(JSDocTag::new("t", " {t6}\nx").r#type(), Some("t6"));
}
#[test]
fn parses_type_comment() {
assert_eq!(JSDocTag::new("r", "{t1} c1").type_comment(), (Some("t1"), "c1".to_string()));
assert_eq!(JSDocTag::new("r", "{t2}").type_comment(), (Some("t2"), String::new()));
assert_eq!(JSDocTag::new("r", "c3").type_comment(), (None, "c3".to_string()));
assert_eq!(JSDocTag::new("r", "c4 foo").type_comment(), (None, "c4 foo".to_string()));
assert_eq!(JSDocTag::new("r", "").type_comment(), (None, String::new()));
assert_eq!(JSDocTag::new("r", " {t1} c1").type_comment(), (Some("t1"), "c1".to_string()));
assert_eq!(JSDocTag::new("r", "\n{t2}").type_comment(), (Some("t2"), String::new()));
assert_eq!(JSDocTag::new("r", " c3").type_comment(), (None, "c3".to_string()));
assert_eq!(JSDocTag::new("r", " c4 foo").type_comment(), (None, "c4 foo".to_string()));
assert_eq!(JSDocTag::new("r", " ").type_comment(), (None, String::new()));
assert_eq!(
JSDocTag::new("r", "{t5}\nc5\n...").type_comment(),
JSDocTag::new("r", "\n{t5}\nc5\n...").type_comment(),
(Some("t5"), "c5\n...".to_string())
);
assert_eq!(
JSDocTag::new("r", "{t6} - c6").type_comment(),
JSDocTag::new("r", " {t6} - c6").type_comment(),
(Some("t6"), "- c6".to_string())
);
assert_eq!(
JSDocTag::new("r", "{{ 型: t7 }} : c7").type_comment(),
JSDocTag::new("r", " {{ 型: t7 }} : c7").type_comment(),
(Some("{ 型: t7 }"), ": c7".to_string())
);
}
@ -174,37 +174,37 @@ mod test {
#[test]
fn parses_type_name_comment() {
assert_eq!(
JSDocTag::new("p", "{t1} n1 c1").type_name_comment(),
JSDocTag::new("p", " {t1} n1 c1").type_name_comment(),
(Some("t1"), Some("n1"), "c1".to_string())
);
assert_eq!(
JSDocTag::new("p", "{t2} n2").type_name_comment(),
JSDocTag::new("p", " {t2} n2").type_name_comment(),
(Some("t2"), Some("n2"), String::new())
);
assert_eq!(
JSDocTag::new("p", "n3 c3").type_name_comment(),
JSDocTag::new("p", " n3 c3").type_name_comment(),
(None, Some("n3"), "c3".to_string())
);
assert_eq!(JSDocTag::new("p", "").type_name_comment(), (None, None, String::new()));
assert_eq!(JSDocTag::new("p", "\n\n").type_name_comment(), (None, None, String::new()));
assert_eq!(
JSDocTag::new("p", "{t4} n4 c4\n...").type_name_comment(),
JSDocTag::new("p", " {t4} n4 c4\n...").type_name_comment(),
(Some("t4"), Some("n4"), "c4\n...".to_string())
);
assert_eq!(
JSDocTag::new("p", "{t5} n5 - c5").type_name_comment(),
JSDocTag::new("p", " {t5} n5 - c5").type_name_comment(),
(Some("t5"), Some("n5"), "- c5".to_string())
);
assert_eq!(
JSDocTag::new("p", "{t6}\nn6\nc6").type_name_comment(),
JSDocTag::new("p", "\n{t6}\nn6\nc6").type_name_comment(),
(Some("t6"), Some("n6"), "c6".to_string())
);
assert_eq!(
JSDocTag::new("p", "{t7}\nn7\nc\n7").type_name_comment(),
JSDocTag::new("p", "\n\n{t7}\nn7\nc\n7").type_name_comment(),
(Some("t7"), Some("n7"), "c\n7".to_string())
);
assert_eq!(
JSDocTag::new("p", "{t8}").type_name_comment(),
JSDocTag::new("p", " {t8}").type_name_comment(),
(Some("t8"), None, String::new())
);
}

View file

@ -1,9 +1,11 @@
use super::jsdoc_tag::JSDocTag;
use super::utils;
use oxc_span::Span;
/// source_text: Inside of /**HERE*/, NOT includes `/**` and `*/`
pub fn parse_jsdoc(source_text: &str) -> (String, Vec<JSDocTag>) {
debug_assert!(!source_text.starts_with("/**"));
/// span_start: Global positioned `Span` start for this JSDoc comment
pub fn parse_jsdoc(source_text: &str, jsdoc_span_start: u32) -> (String, Vec<(Span, JSDocTag)>) {
debug_assert!(!source_text.starts_with("/*"));
debug_assert!(!source_text.ends_with("*/"));
// JSDoc consists of comment and tags.
@ -17,6 +19,7 @@ pub fn parse_jsdoc(source_text: &str) -> (String, Vec<JSDocTag>) {
// But `@` can be found inside of `{}` (e.g. `{@see link}`), it should be distinguished.
let mut in_braces = false;
let mut comment_found = false;
// Parser local offsets, not for global span
let (mut start, mut end) = (0, 0);
for ch in source_text.chars() {
match ch {
@ -26,11 +29,15 @@ pub fn parse_jsdoc(source_text: &str) -> (String, Vec<JSDocTag>) {
let part = &source_text[start..end];
if comment_found {
tags.push(parse_jsdoc_tag(part));
tags.push((
get_tag_kind_span(part, (start, end), jsdoc_span_start),
parse_jsdoc_tag(part),
));
} else {
comment = part;
comment_found = true;
}
// Prepare for the next draft
start = end;
}
@ -45,53 +52,108 @@ pub fn parse_jsdoc(source_text: &str) -> (String, Vec<JSDocTag>) {
let part = &source_text[start..end];
if comment_found {
tags.push(parse_jsdoc_tag(part));
tags.push((
get_tag_kind_span(part, (start, end), jsdoc_span_start),
parse_jsdoc_tag(part),
));
} else {
comment = part;
}
}
(utils::trim_multiline_comment(comment), tags)
(utils::trim_comment(comment), tags)
}
// Use `Span` for `@kind` part instead of whole tag.
//
// For example, whole `tag.span` in the following JSDoc will be:
// /**
// * @kind1 bar
// * baz...
// * @kind2
// */
// for `@kind1`: `@kind1 bar\n * baz...\n * `
// for `@kind2`: `@kind2\n `
//
// It's too verbose and may not fit for linter diagnostics span.
fn get_tag_kind_span(
tag_content: &str,
(tag_offset_start, _): (usize, usize),
jsdoc_span_start: u32,
) -> Span {
debug_assert!(tag_content.starts_with('@'));
// This surely exists, at least `@` itself
let (k_start, k_end) = utils::find_token_range(tag_content).unwrap();
let k_len = k_end - k_start;
let (start, end) = (
u32::try_from(tag_offset_start + k_start).unwrap_or_default(),
u32::try_from(tag_offset_start + k_start + k_len).unwrap_or_default(),
);
Span::new(jsdoc_span_start + start, jsdoc_span_start + end)
}
// TODO: Manage `Span`
// - with (start, end) + global comment span.start
// - add kind only span?
/// tag_content: Starts with `@`, may be mulitline
fn parse_jsdoc_tag(tag_content: &str) -> JSDocTag {
debug_assert!(tag_content.starts_with('@'));
// This surely exists, at least `@` itself
let (k_start, k_end) = utils::find_token_range(tag_content).unwrap();
// +1 for whitespace, may be empty
let b_start = tag_content.len().min(k_end + 1);
// Omit the first `@`
JSDocTag::new(&tag_content[k_start + 1..k_end], &tag_content[b_start..])
JSDocTag::new(
// Omit the first `@`
&tag_content[k_start + 1..k_end],
// Includes splitter whitespace to distinguish these cases:
// ```
// /**
// * @k * <- should not omit
// */
//
// /**
// * @k
// * <- should omit
// */
// ```
// If not included, both body_part will starts with `* <- ...`!
//
// It does not affect the output since it will be trimmed later.
&tag_content[k_end..],
)
}
#[cfg(test)]
mod test {
use super::parse_jsdoc;
use super::parse_jsdoc_tag;
use super::JSDocTag;
fn parse_from_full_text(full_text: &str) -> (String, Vec<JSDocTag>) {
fn parse_from_full_text(full_text: &str) -> (String, Vec<super::JSDocTag>) {
// Outside of markers can be trimmed
let source_text = full_text.trim().trim_start_matches("/**").trim_end_matches("*/");
parse_jsdoc(source_text)
let (comment, tags) = super::parse_jsdoc(source_text, 0);
(comment, tags.iter().map(|(_, t)| t).cloned().collect())
}
#[test]
fn parses_jsdoc_comment() {
assert_eq!(parse_jsdoc("hello source"), ("hello source".to_string(), vec![]));
assert_eq!(parse_from_full_text("/**hello*/"), ("hello".to_string(), vec![]));
assert_eq!(
parse_from_full_text("/** hello full_text */"),
("hello full_text".to_string(), vec![])
);
assert_eq!(parse_from_full_text("/***/"), (String::new(), vec![]));
assert_eq!(parse_from_full_text("/****/"), ("*".to_string(), vec![]));
assert_eq!(parse_from_full_text("/*****/"), ("**".to_string(), vec![]));
assert_eq!(
parse_from_full_text(
"/**
* * x
** y
*/"
)
.0,
"* x\n* y"
);
assert_eq!(parse_jsdoc(" <- trim -> ").0, "<- trim ->");
assert_eq!(parse_from_full_text("/** <- trim -> */").0, "<- trim ->");
assert_eq!(
parse_from_full_text(
"
@ -127,57 +189,52 @@ comment {@link link} ...
);
assert_eq!(
parse_jsdoc("hello {@see inline} source {@a 2}").0,
parse_from_full_text("/**\nhello {@see inline} source {@a 2}\n*/").0,
"hello {@see inline} source {@a 2}"
);
assert_eq!(parse_jsdoc("").0, "");
assert_eq!(parse_from_full_text("/** ハロー @comment だよ*/").0, "ハロー");
}
#[test]
fn parses_single_line_1_jsdoc() {
assert_eq!(parse_jsdoc("@deprecated"), parse_from_full_text("/** @deprecated*/"));
assert_eq!(parse_jsdoc("@deprecated").1, vec![parse_jsdoc_tag("@deprecated")]);
assert_eq!(parse_jsdoc("").1, vec![]);
fn parses_jsdoc_tags() {
assert_eq!(
parse_from_full_text("/**@deprecated*/").1,
vec![parse_jsdoc_tag("@deprecated")]
);
assert_eq!(
parse_from_full_text("/**@foo since 2024 */").1,
vec![parse_jsdoc_tag("@foo since 2024 ")]
);
assert_eq!(parse_from_full_text("/**@*/").1, vec![JSDocTag::new("", "")]);
}
#[test]
fn parses_single_line_n_jsdocs() {
assert_eq!(
parse_from_full_text("/** @foo @bar */").1,
vec![JSDocTag::new("foo", ""), JSDocTag::new("bar", "")]
vec![parse_jsdoc_tag("@foo "), parse_jsdoc_tag("@bar ")]
);
assert_eq!(parse_from_full_text("/**@*/").1, vec![parse_jsdoc_tag("@")]);
assert_eq!(
parse_from_full_text("/** @aiue あいうえ @o お*/").1,
vec![JSDocTag::new("aiue", "あいうえ "), JSDocTag::new("o", "")]
vec![parse_jsdoc_tag("@aiue あいうえ "), parse_jsdoc_tag("@o お")],
);
assert_eq!(
parse_from_full_text("/** @a @@ @d */").1,
vec![
JSDocTag::new("a", ""),
JSDocTag::new("", ""),
JSDocTag::new("", ""),
JSDocTag::new("d", "")
]
parse_jsdoc_tag("@a "),
parse_jsdoc_tag("@"),
parse_jsdoc_tag("@ "),
parse_jsdoc_tag("@d ")
],
);
}
#[test]
fn parses_multiline_1_jsdoc() {
assert_eq!(
parse_from_full_text(
"/** @yo
*/"
)
.1,
vec![JSDocTag::new("yo", " ")]
vec![parse_jsdoc_tag("@yo\n ")]
);
assert_eq!(
parse_from_full_text(
@ -186,7 +243,7 @@ comment {@link link} ...
*/"
)
.1,
vec![JSDocTag::new("foo", " ")]
vec![parse_jsdoc_tag("@foo\n ")]
);
assert_eq!(
parse_from_full_text(
@ -197,7 +254,7 @@ comment {@link link} ...
"
)
.1,
vec![JSDocTag::new("x", "with asterisk\n ")]
vec![parse_jsdoc_tag("@x with asterisk\n ")]
);
assert_eq!(
parse_from_full_text(
@ -209,12 +266,9 @@ comment {@link link} ...
"
)
.1,
vec![JSDocTag::new("y", "without\n asterisk\n ")]
vec![parse_jsdoc_tag("@y without\n asterisk\n ")]
);
}
#[test]
fn parses_multiline_n_jsdocs() {
assert_eq!(
parse_from_full_text(
"
@ -226,9 +280,9 @@ comment {@link link} ...
)
.1,
vec![
JSDocTag::new("foo", ""),
JSDocTag::new("bar", " * "),
JSDocTag::new("baz", " ")
parse_jsdoc_tag("@foo"),
parse_jsdoc_tag("@bar\n * "),
parse_jsdoc_tag("@baz\n ")
]
);
assert_eq!(
@ -242,8 +296,8 @@ comment {@link link} ...
)
.1,
vec![
JSDocTag::new("one", " *\n * ...\n *\n * "),
JSDocTag::new("two", ""),
parse_jsdoc_tag("@one\n *\n * ...\n *\n * "),
parse_jsdoc_tag("@two ")
]
);
assert_eq!(
@ -257,15 +311,52 @@ comment {@link link} ...
)
.1,
vec![
JSDocTag::new(
"hey",
"you!\n * Are you OK?\n * "
parse_jsdoc_tag(
"@hey you!\n * Are you OK?\n * "
),
JSDocTag::new("yes", "I'm fine\n ")
parse_jsdoc_tag("@yes I'm fine\n ")
]
);
}
#[test]
fn parses_practical() {
let jsdoc = parse_from_full_text(
"
/**
* @typedef {Object} User - a User account
* @property {string} displayName - the name used to show the user
* @property {number} id - a unique id
*/
",
);
let mut tags = jsdoc.1.iter();
let tag = tags.next().unwrap();
assert_eq!(tag.kind, "typedef");
let tag = tags.next().unwrap();
assert_eq!(tag.kind, "property");
let tag = tags.next().unwrap();
assert_eq!(tag.kind, "property");
let jsdoc = parse_from_full_text(
"
/**
* Adds two numbers together
* @param {number} a The first number
* @param {number} b The second number
* @returns {number}
*/
",
);
let mut tags = jsdoc.1.iter();
let tag = tags.next().unwrap();
assert_eq!(tag.kind, "param");
let tag = tags.next().unwrap();
assert_eq!(tag.kind, "param");
let tag = tags.next().unwrap();
assert_eq!(tag.kind, "returns");
}
#[test]
fn parses_practical_with_multibyte() {
let jsdoc = parse_from_full_text(

View file

@ -1,7 +1,14 @@
pub fn trim_multiline_comment(s: &str) -> String {
s.trim()
.lines()
.map(|line| line.trim().trim_start_matches('*').trim())
pub fn trim_comment(s: &str) -> String {
let lines = s.lines();
// If single line, there is no leading `*`
if lines.clone().count() == 1 {
return s.trim().to_string();
}
s.lines()
// Trim leading the first `*` in each line
.map(|line| line.trim().strip_prefix('*').unwrap_or(line).trim())
.filter(|line| !line.is_empty())
.collect::<Vec<_>>()
.join("\n")
@ -58,22 +65,33 @@ pub fn find_token_range(s: &str) -> Option<(usize, usize)> {
#[cfg(test)]
mod test {
use super::{find_token_range, find_type_range, trim_multiline_comment};
use super::{find_token_range, find_type_range, trim_comment};
#[test]
fn trim_multiline_jsdoc_comments() {
fn trim_jsdoc_comments() {
for (actual, expect) in [
("", ""),
("hello ", "hello"),
(" * single line", "* single line"),
(" * ", "*"),
(" * * ", "* *"),
("***", "***"),
(
"
trim
", "trim",
),
(
"
", "",
),
("hello", "hello"),
(
"
trim
", "trim",
*
*
",
"",
),
(
"
@ -97,6 +115,13 @@ mod test {
),
(
"
* * 1
** 2
",
"* 1\n* 2",
),
(
"
1
2
@ -107,7 +132,7 @@ mod test {
"1\n2\n3",
),
] {
assert_eq!(trim_multiline_comment(actual), expect);
assert_eq!(trim_comment(actual), expect);
}
}
@ -115,7 +140,7 @@ mod test {
fn extract_type_part_range() {
for (actual, expect) in [
("{t1}", Some("t1")),
("{t2 }", Some("t2 ")),
(" { t2 } ", Some(" t2 ")),
("{{ t3: string }}", Some("{ t3: string }")),
("{t4} name", Some("t4")),
(" {t5} ", Some("t5")),
@ -130,14 +155,14 @@ mod test {
}
#[test]
fn extract_name_part_range() {
fn extract_token_part_range() {
for (actual, expect) in [
("n1", Some("n1")),
("n2 x", Some("n2")),
(" n3 ", Some("n3")),
("n4\ny", Some("n4")),
("", None),
("名前5", Some("名前5")),
(" 名前5\n", Some("名前5")),
("\nn6\nx", Some("n6")),
] {
assert_eq!(find_token_range(actual).map(|(s, e)| &actual[s..e]), expect);