Skip to main content

miwa/
extract.rs

1//! Test extraction for Go, Rust, C#, Java, Kotlin, TypeScript and JavaScript source files.
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub struct ExtractedTest {
5    pub id: String,
6    pub name: String,
7    pub markers: Vec<String>,
8    pub content: String,
9}
10
11#[derive(Debug, thiserror::Error)]
12pub enum ExtractError {
13    #[error("parse error in {path}")]
14    Parse { path: String },
15}
16
17/// Extract Go tests from a `*_test.go` file: top-level `function_declaration`s
18/// whose name starts with `Test`. Markers are collected from the block of
19/// comments immediately (no blank line) preceding the function.
20pub fn extract_go_tests(path: &str, source: &str) -> Result<Vec<ExtractedTest>, ExtractError> {
21    let mut parser = tree_sitter::Parser::new();
22    parser
23        .set_language(&tree_sitter_go::LANGUAGE.into())
24        .expect("valid go grammar");
25    let tree = parser
26        .parse(source, None)
27        .ok_or_else(|| ExtractError::Parse {
28            path: path.to_string(),
29        })?;
30    let root = tree.root_node();
31    if root.has_error() {
32        return Err(ExtractError::Parse {
33            path: path.to_string(),
34        });
35    }
36
37    let dir = dir_of(path);
38    let mut results = Vec::new();
39
40    let mut cursor = root.walk();
41    let children: Vec<tree_sitter::Node> = root.children(&mut cursor).collect();
42    for (idx, node) in children.iter().enumerate() {
43        if node.kind() != "function_declaration" {
44            continue;
45        }
46        let Some(name_node) = node.child_by_field_name("name") else {
47            continue;
48        };
49        let name = name_node.utf8_text(source.as_bytes()).unwrap_or("");
50        if !name.starts_with("Test") {
51            continue;
52        }
53        let markers = collect_preceding_markers(&children, idx, source, "comment");
54        let content = node.utf8_text(source.as_bytes()).unwrap_or("").to_string();
55        results.push(ExtractedTest {
56            id: format!("{dir}:{name}"),
57            name: name.to_string(),
58            markers,
59            content,
60        });
61    }
62    Ok(results)
63}
64
65/// Extract Rust tests: `function_item`s carrying a test attribute, i.e. an
66/// outer attribute whose path ends in `test` (`#[test]`, `#[tokio::test]`, ...)
67/// or in one of `extra_attributes` (last path segment, e.g. `rstest`).
68/// Markers come from the comment block immediately preceding the first
69/// (topmost) test attribute. `content` is the source of the `function_item`
70/// (attributes not included).
71pub fn extract_rust_tests(
72    path: &str,
73    source: &str,
74    extra_attributes: &[String],
75) -> Result<Vec<ExtractedTest>, ExtractError> {
76    let mut parser = tree_sitter::Parser::new();
77    parser
78        .set_language(&tree_sitter_rust::LANGUAGE.into())
79        .expect("valid rust grammar");
80    let tree = parser
81        .parse(source, None)
82        .ok_or_else(|| ExtractError::Parse {
83            path: path.to_string(),
84        })?;
85    let root = tree.root_node();
86    if root.has_error() {
87        return Err(ExtractError::Parse {
88            path: path.to_string(),
89        });
90    }
91
92    let base = path.strip_suffix(".rs").unwrap_or(path);
93    let mut results = Vec::new();
94    walk_rust_items(root, source, base, &[], extra_attributes, &mut results);
95    Ok(results)
96}
97
98fn walk_rust_items(
99    container: tree_sitter::Node,
100    source: &str,
101    base: &str,
102    mods: &[String],
103    extra_attributes: &[String],
104    results: &mut Vec<ExtractedTest>,
105) {
106    let mut cursor = container.walk();
107    let children: Vec<tree_sitter::Node> = container.children(&mut cursor).collect();
108    for (idx, node) in children.iter().enumerate() {
109        match node.kind() {
110            "function_item" => {
111                let Some(name_node) = node.child_by_field_name("name") else {
112                    continue;
113                };
114                let name = name_node
115                    .utf8_text(source.as_bytes())
116                    .unwrap_or("")
117                    .to_string();
118                let Some(markers) = rust_test_markers(&children, idx, source, extra_attributes)
119                else {
120                    continue;
121                };
122                let content = node.utf8_text(source.as_bytes()).unwrap_or("").to_string();
123                let id = if mods.is_empty() {
124                    format!("{base}::{name}")
125                } else {
126                    format!("{base}:{}:{name}", mods.join("/"))
127                };
128                results.push(ExtractedTest {
129                    id,
130                    name,
131                    markers,
132                    content,
133                });
134            }
135            "mod_item" => {
136                let Some(name_node) = node.child_by_field_name("name") else {
137                    continue;
138                };
139                let Some(body) = node.child_by_field_name("body") else {
140                    continue;
141                };
142                let mod_name = name_node
143                    .utf8_text(source.as_bytes())
144                    .unwrap_or("")
145                    .to_string();
146                let mut new_mods = mods.to_vec();
147                new_mods.push(mod_name);
148                walk_rust_items(body, source, base, &new_mods, extra_attributes, results);
149            }
150            _ => {}
151        }
152    }
153}
154
155/// Last row a comment occupies. tree-sitter-rust includes the trailing newline
156/// of `///` / `//!` line comments, so such a node ends at column 0 of the
157/// next row; that row is not part of the comment.
158fn comment_end_row(node: tree_sitter::Node) -> usize {
159    let end = node.end_position();
160    if end.column == 0 && end.row > node.start_position().row {
161        end.row - 1
162    } else {
163        end.row
164    }
165}
166
167/// Whether only whitespace (or a BOM) precedes the node on its line in
168/// `parsed` (the string the tree was built from). FR-064.
169fn is_own_line_comment(parsed: &str, node: tree_sitter::Node) -> bool {
170    let s = node.start_byte();
171    let Some(head) = parsed.get(..s) else {
172        return false;
173    };
174    let ls = head.rfind('\n').map_or(0, |i| i + 1);
175    head[ls..]
176        .chars()
177        .all(|c| c.is_whitespace() || c == '\u{feff}')
178}
179
180/// Walks backward from `children[target_idx]`, collecting a contiguous
181/// (no-blank-line) block of own-line comment nodes whose kind is in
182/// `comment_kinds`, and extracts `[marker]` tokens from them, top to bottom.
183/// A comment that does not start its line (e.g. trailing a previous item)
184/// ends the block (FR-064). `parsed` is the string the tree was parsed from;
185/// `text_of` yields the text of a node (Kotlin reads the original source, not
186/// its ASCII shadow). Nodes for which `is_doc` holds are read over without
187/// contributing markers and without ending the block.
188fn collect_preceding_markers_with<'a>(
189    children: &[tree_sitter::Node],
190    target_idx: usize,
191    parsed: &str,
192    text_of: &dyn Fn(tree_sitter::Node) -> Option<&'a str>,
193    comment_kinds: &[&str],
194    is_doc: &dyn Fn(tree_sitter::Node) -> bool,
195) -> Vec<String> {
196    let mut next_start_row = children[target_idx].start_position().row;
197    let mut j = target_idx as isize - 1;
198    let mut comments = Vec::new();
199    while j >= 0 {
200        let node = children[j as usize];
201        if !comment_kinds.contains(&node.kind()) {
202            break;
203        }
204        let Some(text) = text_of(node) else {
205            break;
206        };
207        if comment_end_row(node) + 1 != next_start_row || !is_own_line_comment(parsed, node) {
208            break;
209        }
210        if !is_doc(node) {
211            comments.push(text);
212        }
213        next_start_row = node.start_position().row;
214        j -= 1;
215    }
216    comments.reverse();
217    let mut markers = Vec::new();
218    for text in comments {
219        markers.extend(extract_markers(text));
220    }
221    markers
222}
223
224/// Single-kind variant of [`collect_preceding_markers_with`] (no doc skip).
225fn collect_preceding_markers(
226    children: &[tree_sitter::Node],
227    target_idx: usize,
228    source: &str,
229    comment_kind: &str,
230) -> Vec<String> {
231    collect_preceding_markers_with(
232        children,
233        target_idx,
234        source,
235        &|n| n.utf8_text(source.as_bytes()).ok(),
236        &[comment_kind],
237        &|_| false,
238    )
239}
240
241/// Rust doc comments (`///`, `//!`, `/** */`, `/*! */`) carry an
242/// `outer_doc_comment_marker` / `inner_doc_comment_marker` child. Decided by
243/// node structure so `////`, `/***` and `/**/` stay ordinary comments.
244fn is_rust_doc_comment(node: tree_sitter::Node) -> bool {
245    let mut cursor = node.walk();
246    let found = node.children(&mut cursor).any(|c| {
247        matches!(
248            c.kind(),
249            "outer_doc_comment_marker" | "inner_doc_comment_marker"
250        )
251    });
252    found
253}
254
255/// Returns `None` if the attribute chain above `children[func_idx]` has no
256/// test attribute. The chain is the contiguous run of `attribute_item`s and
257/// comments directly above the `fn` (comments do not end the chain). A test
258/// attribute is one whose path's last segment is `test` or listed in
259/// `extra_attributes`. Otherwise returns the markers from the comment block
260/// immediately (no blank line) preceding the topmost test attribute.
261fn rust_test_markers(
262    children: &[tree_sitter::Node],
263    func_idx: usize,
264    source: &str,
265    extra_attributes: &[String],
266) -> Option<Vec<String>> {
267    let mut test_attr_idx: Option<usize> = None;
268    let mut j = func_idx as isize - 1;
269    while j >= 0 {
270        let node = children[j as usize];
271        if node.kind() == "attribute_item" {
272            if rust_is_test_attribute(node, source, extra_attributes) {
273                test_attr_idx = Some(j as usize);
274            }
275        } else if !RUST_COMMENT_KINDS.contains(&node.kind()) {
276            break;
277        }
278        j -= 1;
279    }
280    let test_attr_idx = test_attr_idx?;
281    Some(collect_preceding_markers_with(
282        children,
283        test_attr_idx,
284        source,
285        &|n| n.utf8_text(source.as_bytes()).ok(),
286        &RUST_COMMENT_KINDS,
287        &is_rust_doc_comment,
288    ))
289}
290
291/// Whether an `attribute_item` is a test attribute. The attribute path's last
292/// segment is the text of the first named child of `attribute` when it is an
293/// `identifier`, or the `name` field when it is a `scoped_identifier`;
294/// arguments are never inspected. It matches when that segment is `test` or
295/// equals one of `extra_attributes`. `#![test]` is an `inner_attribute_item`,
296/// so it never reaches this function.
297fn rust_is_test_attribute(
298    item: tree_sitter::Node,
299    source: &str,
300    extra_attributes: &[String],
301) -> bool {
302    let mut cursor = item.walk();
303    let Some(attribute) = item
304        .named_children(&mut cursor)
305        .find(|c| c.kind() == "attribute")
306    else {
307        return false;
308    };
309    let Some(path) = attribute.named_child(0) else {
310        return false;
311    };
312    let segment = match path.kind() {
313        "identifier" => path,
314        "scoped_identifier" => match path.child_by_field_name("name") {
315            Some(n) => n,
316            None => return false,
317        },
318        _ => return false,
319    };
320    let text = segment.utf8_text(source.as_bytes()).unwrap_or("");
321    text == "test" || extra_attributes.iter().any(|a| a == text)
322}
323
324const RUST_COMMENT_KINDS: [&str; 2] = ["line_comment", "block_comment"];
325
326/// Doc comments (Javadoc / KDoc) are never markers in Java / Kotlin (and do
327/// not end the adjacent comment block).
328fn is_doc_comment(text: &str) -> bool {
329    text.starts_with("/**")
330}
331
332/// Test names that are empty or contain control characters / line separators
333/// (U+0085, U+2028, U+2029) are ignored.
334fn is_valid_test_name(name: &str) -> bool {
335    !name.is_empty()
336        && !name
337            .chars()
338            .any(|c| c.is_control() || matches!(c, '\u{85}' | '\u{2028}' | '\u{2029}'))
339}
340
341/// Strips the comment syntax once per kind and returns the body together with
342/// whether it is a block comment body. Line comments lose `//` and at most one
343/// more `/` (`///`); block comments lose `/*`, at most one more `*` (`/**`) and
344/// the closing `*/`. Any further `/` or `*` stays in the body.
345fn comment_body(text: &str) -> (&str, bool) {
346    if let Some(rest) = text.strip_prefix("//") {
347        return (rest.strip_prefix('/').unwrap_or(rest), false);
348    }
349    match text.strip_prefix("/*") {
350        Some(rest) => {
351            let rest = rest.strip_suffix("*/").unwrap_or(rest);
352            (rest.strip_prefix('*').unwrap_or(rest), true)
353        }
354        None => (text, false),
355    }
356}
357
358/// Returns the markers of one comment. The comment is a marker comment only
359/// when its body (comment syntax and surrounding whitespace removed) consists
360/// solely of whitespace-separated `[marker]` tokens, where `marker` is one or
361/// more `[A-Za-z0-9._-]` characters. Any other text makes it a plain comment
362/// with no markers. FR-121.
363fn extract_markers(text: &str) -> Vec<String> {
364    let (body, is_block) = comment_body(text);
365    let mut markers = Vec::new();
366    for (i, line) in body.lines().enumerate() {
367        // Continuation lines of a block comment may start with one `*`.
368        let line = match (is_block && i > 0, line.trim_start().strip_prefix('*')) {
369            (true, Some(rest)) => rest,
370            _ => line,
371        };
372        for token in line.split_whitespace() {
373            let Some(inner) = token
374                .strip_prefix('[')
375                .and_then(|t| t.strip_suffix(']'))
376                .filter(|inner| {
377                    !inner.is_empty()
378                        && inner
379                            .chars()
380                            .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '-')
381                })
382            else {
383                return Vec::new();
384            };
385            markers.push(inner.to_string());
386        }
387    }
388    markers
389}
390
391fn dir_of(path: &str) -> String {
392    match path.rsplit_once('/') {
393        Some((dir, _)) => dir.to_string(),
394        None => ".".to_string(),
395    }
396}
397
398const CSHARP_TEST_ATTRIBUTES: [&str; 7] = [
399    "Fact",
400    "Theory",
401    "Test",
402    "TestCase",
403    "TestCaseSource",
404    "TestMethod",
405    "DataTestMethod",
406];
407
408/// Extract C# tests (xUnit / NUnit / MSTest) from a `*.cs` file: methods inside
409/// types (also through namespaces and `#if` blocks) carrying a test attribute.
410/// Markers come from the comment block immediately preceding the method, and
411/// `content` runs from the first non-attribute token to the end of the method.
412pub fn extract_csharp_tests(path: &str, source: &str) -> Result<Vec<ExtractedTest>, ExtractError> {
413    let mut parser = tree_sitter::Parser::new();
414    parser
415        .set_language(&tree_sitter_c_sharp::LANGUAGE.into())
416        .expect("valid c# grammar");
417    let tree = parser
418        .parse(source, None)
419        .ok_or_else(|| ExtractError::Parse {
420            path: path.to_string(),
421        })?;
422    let root = tree.root_node();
423    if root.has_error() {
424        return Err(ExtractError::Parse {
425            path: path.to_string(),
426        });
427    }
428    let base = path.strip_suffix(".cs").unwrap_or(path);
429    let mut results = Vec::new();
430    walk_csharp_container(root, source, base, &[], &mut results);
431    Ok(results)
432}
433
434fn walk_csharp_container(
435    container: tree_sitter::Node,
436    source: &str,
437    base: &str,
438    types: &[String],
439    results: &mut Vec<ExtractedTest>,
440) {
441    let mut cursor = container.walk();
442    let children: Vec<tree_sitter::Node> = container.children(&mut cursor).collect();
443    for (idx, node) in children.iter().enumerate() {
444        match node.kind() {
445            "preproc_if" | "preproc_elif" | "preproc_else" => {
446                walk_csharp_container(*node, source, base, types, results);
447            }
448            "namespace_declaration" => {
449                if let Some(body) = node.child_by_field_name("body") {
450                    walk_csharp_container(body, source, base, types, results);
451                }
452            }
453            "class_declaration"
454            | "struct_declaration"
455            | "record_declaration"
456            | "interface_declaration" => {
457                let (Some(name_node), Some(body)) = (
458                    node.child_by_field_name("name"),
459                    node.child_by_field_name("body"),
460                ) else {
461                    continue;
462                };
463                let name = name_node.utf8_text(source.as_bytes()).unwrap_or("");
464                let mut new_types = types.to_vec();
465                new_types.push(name.to_string());
466                walk_csharp_container(body, source, base, &new_types, results);
467            }
468            "method_declaration" if !types.is_empty() => {
469                if !csharp_is_test_method(*node, source) {
470                    continue;
471                }
472                let Some(name_node) = node.child_by_field_name("name") else {
473                    continue;
474                };
475                let name = name_node
476                    .utf8_text(source.as_bytes())
477                    .unwrap_or("")
478                    .to_string();
479                let mut c = node.walk();
480                let Some(first) = node
481                    .children(&mut c)
482                    .find(|ch| ch.kind() != "attribute_list" && ch.kind() != "comment")
483                else {
484                    continue;
485                };
486                let content = source[first.start_byte()..node.end_byte()].to_string();
487                let markers = collect_preceding_markers(&children, idx, source, "comment");
488                results.push(ExtractedTest {
489                    id: format!("{base}:{}:{name}", types.join("/")),
490                    name,
491                    markers,
492                    content,
493                });
494            }
495            _ => {}
496        }
497    }
498}
499
500fn csharp_is_test_method(method: tree_sitter::Node, source: &str) -> bool {
501    let mut cursor = method.walk();
502    for list in method.children(&mut cursor) {
503        if list.kind() != "attribute_list" {
504            continue;
505        }
506        let mut c2 = list.walk();
507        for attr in list.children(&mut c2) {
508            if attr.kind() != "attribute" {
509                continue;
510            }
511            let Some(mut name) = attr.child_by_field_name("name") else {
512                continue;
513            };
514            while name.kind() == "qualified_name" {
515                match name.child_by_field_name("name") {
516                    Some(n) => name = n,
517                    None => break,
518                }
519            }
520            let text = name.utf8_text(source.as_bytes()).unwrap_or("");
521            if CSHARP_TEST_ATTRIBUTES.contains(&text) {
522                return true;
523            }
524        }
525    }
526    false
527}
528
529const TYPESCRIPT_TEST_MODIFIERS: [&str; 6] =
530    ["skip", "only", "todo", "fails", "concurrent", "sequential"];
531const TYPESCRIPT_DESCRIBE_MODIFIERS: [&str; 6] = [
532    "skip",
533    "only",
534    "todo",
535    "concurrent",
536    "sequential",
537    "shuffle",
538];
539
540/// File extensions (without the dot) of TypeScript / JavaScript (Vitest)
541/// test files: `<name>.test.<ext>` / `<name>.spec.<ext>`.
542pub const TYPESCRIPT_TEST_EXTENSIONS: [&str; 8] =
543    ["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
544
545/// Whether `path` is a TypeScript / JavaScript (Vitest) test file, i.e. ends
546/// with `.test.<ext>` or `.spec.<ext>` for one of the supported extensions.
547pub fn is_typescript_test_file(path: &str) -> bool {
548    TYPESCRIPT_TEST_EXTENSIONS.iter().any(|ext| {
549        path.ends_with(&format!(".test.{ext}")) || path.ends_with(&format!(".spec.{ext}"))
550    })
551}
552
553/// Extract TypeScript / JavaScript (Vitest) tests from a `*.test.<ext>` /
554/// `*.spec.<ext>` file (see [`TYPESCRIPT_TEST_EXTENSIONS`]): `it` / `test`
555/// call statements at the top level or directly inside `describe` callbacks.
556/// Markers come from the comment block immediately preceding the call, and
557/// `content` is the whole call statement.
558pub fn extract_typescript_tests(
559    path: &str,
560    source: &str,
561) -> Result<Vec<ExtractedTest>, ExtractError> {
562    let mut parser = tree_sitter::Parser::new();
563    let lang: tree_sitter::Language = if [".ts", ".mts", ".cts"]
564        .iter()
565        .any(|ext| path.ends_with(ext))
566    {
567        tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()
568    } else {
569        tree_sitter_typescript::LANGUAGE_TSX.into()
570    };
571    parser
572        .set_language(&lang)
573        .expect("valid typescript grammar");
574    let tree = parser
575        .parse(source, None)
576        .ok_or_else(|| ExtractError::Parse {
577            path: path.to_string(),
578        })?;
579    let root = tree.root_node();
580    if root.has_error() {
581        return Err(ExtractError::Parse {
582            path: path.to_string(),
583        });
584    }
585    let base = TYPESCRIPT_TEST_EXTENSIONS
586        .iter()
587        .find_map(|ext| path.strip_suffix(&format!(".{ext}")))
588        .unwrap_or(path);
589    let mut results = Vec::new();
590    walk_typescript_statements(root, source, base, &[], &mut results);
591    Ok(results)
592}
593
594/// Returns the base identifier and the property chain of a callee made only
595/// of identifiers and member accesses (`test.concurrent.only`).
596fn typescript_callee_chain<'a>(
597    callee: tree_sitter::Node,
598    source: &'a str,
599) -> Option<(&'a str, Vec<&'a str>)> {
600    let mut props = Vec::new();
601    let mut cur = callee;
602    loop {
603        match cur.kind() {
604            "identifier" => {
605                props.reverse();
606                return Some((cur.utf8_text(source.as_bytes()).ok()?, props));
607            }
608            "member_expression" => {
609                let mut c = cur.walk();
610                if cur.children(&mut c).any(|ch| ch.kind() == "optional_chain") {
611                    return None;
612                }
613                let prop = cur.child_by_field_name("property")?;
614                if prop.kind() != "property_identifier" {
615                    return None;
616                }
617                props.push(prop.utf8_text(source.as_bytes()).ok()?);
618                cur = cur.child_by_field_name("object")?;
619            }
620            _ => return None,
621        }
622    }
623}
624
625/// First-argument name: a plain string or a substitution-free template
626/// string, sliced from the source without interpreting escapes.
627fn typescript_name_arg<'a>(args: tree_sitter::Node, source: &'a str) -> Option<&'a str> {
628    let first = args.named_child(0)?;
629    match first.kind() {
630        "string" => {}
631        "template_string" => {
632            let mut c = first.walk();
633            if first
634                .children(&mut c)
635                .any(|ch| ch.kind() == "template_substitution")
636            {
637                return None;
638            }
639        }
640        _ => return None,
641    }
642    let text = first.utf8_text(source.as_bytes()).ok()?;
643    if text.len() < 2 {
644        return None;
645    }
646    let name = &text[1..text.len() - 1];
647    if !is_valid_test_name(name) {
648        return None;
649    }
650    Some(name)
651}
652
653fn walk_typescript_statements(
654    container: tree_sitter::Node,
655    source: &str,
656    base: &str,
657    describes: &[String],
658    results: &mut Vec<ExtractedTest>,
659) {
660    let mut cursor = container.walk();
661    let children: Vec<tree_sitter::Node> = container.children(&mut cursor).collect();
662    for (idx, node) in children.iter().enumerate() {
663        if node.kind() != "expression_statement" {
664            continue;
665        }
666        let Some(call) = node.named_child(0) else {
667            continue;
668        };
669        if call.kind() != "call_expression" {
670            continue;
671        }
672        let (Some(callee), Some(args)) = (
673            call.child_by_field_name("function"),
674            call.child_by_field_name("arguments"),
675        ) else {
676            continue;
677        };
678        let Some((head, props)) = typescript_callee_chain(callee, source) else {
679            continue;
680        };
681        let Some(name) = typescript_name_arg(args, source) else {
682            continue;
683        };
684        if (head == "test" || head == "it")
685            && props.iter().all(|p| TYPESCRIPT_TEST_MODIFIERS.contains(p))
686        {
687            let markers = collect_preceding_markers(&children, idx, source, "comment");
688            let content = node.utf8_text(source.as_bytes()).unwrap_or("").to_string();
689            results.push(ExtractedTest {
690                id: format!("{base}:{}:{name}", describes.join("/")),
691                name: name.to_string(),
692                markers,
693                content,
694            });
695        } else if head == "describe"
696            && props
697                .iter()
698                .all(|p| TYPESCRIPT_DESCRIBE_MODIFIERS.contains(p))
699        {
700            let Some(cb) = args.named_child(1) else {
701                continue;
702            };
703            if cb.kind() != "arrow_function" && cb.kind() != "function_expression" {
704                continue;
705            }
706            let Some(body) = cb.child_by_field_name("body") else {
707                continue;
708            };
709            if body.kind() != "statement_block" {
710                continue;
711            }
712            let mut new_desc = describes.to_vec();
713            new_desc.push(name.to_string());
714            walk_typescript_statements(body, source, base, &new_desc, results);
715        }
716    }
717}
718
719// ---- Java / Kotlin (JUnit) ----
720
721/// Annotation names (last identifier, qualifiers ignored) marking a JUnit
722/// style test method / function in Java and Kotlin.
723const JVM_TEST_ANNOTATIONS: [&str; 5] = [
724    "Test",
725    "ParameterizedTest",
726    "RepeatedTest",
727    "TestFactory",
728    "TestTemplate",
729];
730
731const JVM_COMMENT_KINDS: [&str; 2] = ["line_comment", "block_comment"];
732
733/// Start byte of the hashed range: the first token of the declaration that is
734/// neither an annotation nor a comment (annotations / comments that lead the
735/// modifiers and the comments right after them are skipped).
736fn jvm_content_start(decl: tree_sitter::Node) -> Option<usize> {
737    fn skip(kind: &str) -> bool {
738        matches!(
739            kind,
740            "annotation" | "marker_annotation" | "line_comment" | "block_comment"
741        )
742    }
743    let mut cursor = decl.walk();
744    for child in decl.children(&mut cursor) {
745        if child.kind() == "modifiers" {
746            let mut c2 = child.walk();
747            let first = child.children(&mut c2).find(|n| !skip(n.kind()));
748            if let Some(first) = first {
749                return Some(first.start_byte());
750            }
751        } else if !skip(child.kind()) {
752            return Some(child.start_byte());
753        }
754    }
755    None
756}
757
758fn jvm_name_is_test(name: &str) -> bool {
759    JVM_TEST_ANNOTATIONS.contains(&name)
760}
761
762// -- Java --
763
764/// Extract Java (JUnit) tests: methods annotated with one of
765/// [`JVM_TEST_ANNOTATIONS`] directly inside class / record / interface / enum
766/// bodies. Markers come from the `//` / `/* */` comment block immediately
767/// preceding the method; `content` starts at the first non-annotation token.
768pub fn extract_java_tests(path: &str, source: &str) -> Result<Vec<ExtractedTest>, ExtractError> {
769    let mut parser = tree_sitter::Parser::new();
770    parser
771        .set_language(&tree_sitter_java::LANGUAGE.into())
772        .expect("valid java grammar");
773    let tree = parser
774        .parse(source, None)
775        .ok_or_else(|| ExtractError::Parse {
776            path: path.to_string(),
777        })?;
778    let root = tree.root_node();
779    if root.has_error() {
780        return Err(ExtractError::Parse {
781            path: path.to_string(),
782        });
783    }
784    let base = path.strip_suffix(".java").unwrap_or(path);
785    let mut results = Vec::new();
786    walk_java_container(root, source, base, &[], &mut results);
787    Ok(results)
788}
789
790fn walk_java_container(
791    container: tree_sitter::Node,
792    source: &str,
793    base: &str,
794    types: &[String],
795    results: &mut Vec<ExtractedTest>,
796) {
797    let mut cursor = container.walk();
798    let children: Vec<tree_sitter::Node> = container.children(&mut cursor).collect();
799    for (idx, node) in children.iter().enumerate() {
800        match node.kind() {
801            "class_declaration"
802            | "record_declaration"
803            | "interface_declaration"
804            | "enum_declaration" => {
805                let (Some(name_node), Some(body)) = (
806                    node.child_by_field_name("name"),
807                    node.child_by_field_name("body"),
808                ) else {
809                    continue;
810                };
811                let name = name_node.utf8_text(source.as_bytes()).unwrap_or("");
812                let mut new_types = types.to_vec();
813                new_types.push(name.to_string());
814                if body.kind() == "enum_body" {
815                    let mut c = body.walk();
816                    let decls: Vec<_> = body
817                        .children(&mut c)
818                        .filter(|n| n.kind() == "enum_body_declarations")
819                        .collect();
820                    for d in decls {
821                        walk_java_container(d, source, base, &new_types, results);
822                    }
823                } else {
824                    walk_java_container(body, source, base, &new_types, results);
825                }
826            }
827            "method_declaration" if !types.is_empty() => {
828                if !java_is_test_method(*node, source) {
829                    continue;
830                }
831                let Some(name_node) = node.child_by_field_name("name") else {
832                    continue;
833                };
834                let Some(start) = jvm_content_start(*node) else {
835                    continue;
836                };
837                let name = name_node
838                    .utf8_text(source.as_bytes())
839                    .unwrap_or("")
840                    .to_string();
841                results.push(ExtractedTest {
842                    id: format!("{base}:{}:{name}", types.join("/")),
843                    name,
844                    markers: collect_preceding_markers_with(
845                        &children,
846                        idx,
847                        source,
848                        &|n| n.utf8_text(source.as_bytes()).ok(),
849                        &JVM_COMMENT_KINDS,
850                        &|n| n.utf8_text(source.as_bytes()).is_ok_and(is_doc_comment),
851                    ),
852                    content: source[start..node.end_byte()].to_string(),
853                });
854            }
855            _ => {}
856        }
857    }
858}
859
860fn java_is_test_method(method: tree_sitter::Node, source: &str) -> bool {
861    let mut cursor = method.walk();
862    let Some(modifiers) = method
863        .children(&mut cursor)
864        .find(|n| n.kind() == "modifiers")
865    else {
866        return false;
867    };
868    let mut c2 = modifiers.walk();
869    for ann in modifiers.children(&mut c2) {
870        if ann.kind() != "marker_annotation" && ann.kind() != "annotation" {
871            continue;
872        }
873        let Some(mut name) = ann.child_by_field_name("name") else {
874            continue;
875        };
876        while name.kind() == "scoped_identifier" {
877            match name.child_by_field_name("name") {
878                Some(n) => name = n,
879                None => break,
880            }
881        }
882        if jvm_name_is_test(name.utf8_text(source.as_bytes()).unwrap_or("")) {
883            return true;
884        }
885    }
886    false
887}
888
889// -- Kotlin --
890
891/// The Kotlin grammar rejects identifiers with many non-Latin characters.
892/// The source is therefore parsed as an "ASCII shadow": every non-ASCII
893/// code point becomes one ASCII character (`' '` for whitespace and U+FEFF,
894/// `'x'` otherwise). `map` records `(shadow_offset, original_offset)` pairs at
895/// every point where the two offsets diverge, so shadow byte ranges can be
896/// mapped back to the original source.
897struct KotlinShadow<'a> {
898    original: &'a str,
899    shadow: String,
900    map: Vec<(usize, usize)>,
901}
902
903impl<'a> KotlinShadow<'a> {
904    fn new(original: &'a str) -> Self {
905        let mut shadow = String::with_capacity(original.len());
906        let mut map = vec![(0usize, 0usize)];
907        for (off, ch) in original.char_indices() {
908            if ch.is_ascii() {
909                shadow.push(ch);
910            } else {
911                shadow.push(if ch.is_whitespace() || ch == '\u{feff}' {
912                    ' '
913                } else {
914                    'x'
915                });
916                map.push((shadow.len(), off + ch.len_utf8()));
917            }
918        }
919        KotlinShadow {
920            original,
921            shadow,
922            map,
923        }
924    }
925
926    /// Replaces the given shadow byte ranges with spaces (newlines kept).
927    fn blank_ranges(&mut self, ranges: &[(usize, usize)]) {
928        let mut bytes = std::mem::take(&mut self.shadow).into_bytes();
929        for &(a, b) in ranges {
930            for byte in &mut bytes[a..b] {
931                if *byte != b'\n' {
932                    *byte = b' ';
933                }
934            }
935        }
936        self.shadow = String::from_utf8(bytes).expect("ascii shadow");
937    }
938
939    fn to_original(&self, shadow_offset: usize) -> usize {
940        let i = self.map.partition_point(|&(s, _)| s <= shadow_offset) - 1;
941        let (s, o) = self.map[i];
942        o + (shadow_offset - s)
943    }
944
945    fn slice(&self, start: usize, end: usize) -> Option<&'a str> {
946        self.original
947            .get(self.to_original(start)..self.to_original(end))
948    }
949
950    fn node_text(&self, node: tree_sitter::Node) -> Option<&'a str> {
951        self.slice(node.start_byte(), node.end_byte())
952    }
953}
954
955/// Extract Kotlin (JUnit) tests: functions annotated with one of
956/// [`JVM_TEST_ANNOTATIONS`] at the top level or directly inside class /
957/// object / companion object bodies. Parsing uses an ASCII shadow of the
958/// source (see [`KotlinShadow`]); names and `content` come from the original.
959pub fn extract_kotlin_tests(path: &str, source: &str) -> Result<Vec<ExtractedTest>, ExtractError> {
960    kotlin_extract(path, source, true)
961}
962
963/// Top-level node kinds the Kotlin grammar may legitimately produce under
964/// `source_file`. Anything else (e.g. a leftover `annotated_expression`)
965/// means the grammar failed to recognise a declaration.
966const KOTLIN_ROOT_KINDS: [&str; 11] = [
967    "shebang",
968    "file_annotation",
969    "package_header",
970    "import",
971    "class_declaration",
972    "object_declaration",
973    "function_declaration",
974    "property_declaration",
975    "type_alias",
976    "line_comment",
977    "block_comment",
978];
979
980/// `retry_annotated_expression` enables the class-annotation blanking
981/// workaround (disabled only by tests of the fallback error path).
982fn kotlin_extract(
983    path: &str,
984    source: &str,
985    retry_annotated_expression: bool,
986) -> Result<Vec<ExtractedTest>, ExtractError> {
987    let parse_err = || ExtractError::Parse {
988        path: path.to_string(),
989    };
990    let mut shadow = KotlinShadow::new(source);
991    let mut parser = tree_sitter::Parser::new();
992    parser
993        .set_language(&tree_sitter_kotlin_ng::LANGUAGE.into())
994        .expect("valid kotlin grammar");
995    let mut tree = parser.parse(&shadow.shadow, None).ok_or_else(parse_err)?;
996    if tree.root_node().has_error() {
997        return Err(parse_err());
998    }
999    // Grammar bugs that leave an `annotated_expression` at file level with no
1000    // error: (1) a class with 2+ annotations, no supertype and an
1001    // `@Annotation inner class` in its body; (2) an annotation with
1002    // positional arguments (`@S("L")`) after a top-level declaration and a
1003    // comment. The annotations are not needed for extraction, so blank them
1004    // out and parse again.
1005    if retry_annotated_expression && kotlin_has_annotated_expression(tree.root_node()) {
1006        let ranges = kotlin_annotated_expression_ranges(tree.root_node());
1007        drop(tree);
1008        shadow.blank_ranges(&ranges);
1009        tree = parser.parse(&shadow.shadow, None).ok_or_else(parse_err)?;
1010        if tree.root_node().has_error() {
1011            return Err(parse_err());
1012        }
1013    }
1014    let root = tree.root_node();
1015    // A structure other than declarations at file level means the grammar
1016    // missed a declaration: never silently report zero tests.
1017    let mut cursor = root.walk();
1018    let unexpected = root
1019        .children(&mut cursor)
1020        .any(|n| !KOTLIN_ROOT_KINDS.contains(&n.kind()));
1021    if unexpected {
1022        return Err(parse_err());
1023    }
1024    let base = path.strip_suffix(".kt").unwrap_or(path);
1025    let mut results = Vec::new();
1026    walk_kotlin_container(root, &shadow, base, &[], &mut results).map_err(|()| parse_err())?;
1027    Ok(results)
1028}
1029
1030fn kotlin_has_annotated_expression(root: tree_sitter::Node) -> bool {
1031    let mut cursor = root.walk();
1032    let found = root
1033        .children(&mut cursor)
1034        .any(|n| n.kind() == "annotated_expression");
1035    found
1036}
1037
1038/// Byte ranges to blank for each file-level `annotated_expression`. With
1039/// positional arguments (`@S("L")`) the grammar splits the annotation into an
1040/// `annotation` (`@S`) and a `parenthesized_expression` (`("L")`); only the
1041/// latter is blanked, which leaves a plain annotation that parses as part of
1042/// the declaration's modifiers (so marker adjacency still sees its first
1043/// row). Otherwise (the class case) the `annotation` children are blanked. The
1044/// expression as a whole is never blanked: in the class case it spans the
1045/// entire class body.
1046fn kotlin_annotated_expression_ranges(root: tree_sitter::Node) -> Vec<(usize, usize)> {
1047    let mut ranges = Vec::new();
1048    let mut cursor = root.walk();
1049    for expr in root.children(&mut cursor) {
1050        if expr.kind() != "annotated_expression" {
1051            continue;
1052        }
1053        let mut c2 = expr.walk();
1054        let children: Vec<tree_sitter::Node> = expr.children(&mut c2).collect();
1055        let args: Vec<_> = children
1056            .windows(2)
1057            .filter(|w| w[0].kind() == "annotation" && w[1].kind() == "parenthesized_expression")
1058            .map(|w| &w[1])
1059            .collect();
1060        let picked: Vec<_> = if args.is_empty() {
1061            children
1062                .iter()
1063                .filter(|c| c.kind() == "annotation")
1064                .collect()
1065        } else {
1066            args
1067        };
1068        ranges.extend(picked.iter().map(|n| (n.start_byte(), n.end_byte())));
1069    }
1070    ranges
1071}
1072
1073fn kotlin_ident(text: &str) -> String {
1074    text.strip_prefix('`')
1075        .and_then(|t| t.strip_suffix('`'))
1076        .unwrap_or(text)
1077        .to_string()
1078}
1079
1080fn walk_kotlin_container(
1081    container: tree_sitter::Node,
1082    shadow: &KotlinShadow,
1083    base: &str,
1084    types: &[String],
1085    results: &mut Vec<ExtractedTest>,
1086) -> Result<(), ()> {
1087    let mut cursor = container.walk();
1088    let children: Vec<tree_sitter::Node> = container.children(&mut cursor).collect();
1089    for (idx, node) in children.iter().enumerate() {
1090        match node.kind() {
1091            "class_declaration" | "object_declaration" | "companion_object" => {
1092                let name = match node.child_by_field_name("name") {
1093                    Some(n) => match shadow.node_text(n) {
1094                        Some(t) => kotlin_ident(t),
1095                        None => {
1096                            debug_assert!(false, "kotlin name range off a char boundary");
1097                            return Err(());
1098                        }
1099                    },
1100                    None if node.kind() == "companion_object" => "Companion".to_string(),
1101                    None => continue,
1102                };
1103                let mut c = node.walk();
1104                let Some(body) = node
1105                    .children(&mut c)
1106                    .find(|n| matches!(n.kind(), "class_body" | "enum_class_body"))
1107                else {
1108                    continue;
1109                };
1110                let mut new_types = types.to_vec();
1111                new_types.push(name);
1112                walk_kotlin_container(body, shadow, base, &new_types, results)?;
1113            }
1114            "function_declaration" => {
1115                if !kotlin_is_test_function(*node, shadow) {
1116                    continue;
1117                }
1118                let Some(name_node) = node.child_by_field_name("name") else {
1119                    continue;
1120                };
1121                let Some(name_text) = shadow.node_text(name_node) else {
1122                    debug_assert!(false, "kotlin name range off a char boundary");
1123                    return Err(());
1124                };
1125                let name = kotlin_ident(name_text);
1126                if !is_valid_test_name(&name) {
1127                    continue;
1128                }
1129                let Some(start) = jvm_content_start(*node) else {
1130                    continue;
1131                };
1132                let Some(content) = shadow.slice(start, node.end_byte()) else {
1133                    debug_assert!(false, "kotlin content range off a char boundary");
1134                    return Err(());
1135                };
1136                results.push(ExtractedTest {
1137                    id: format!("{base}:{}:{name}", types.join("/")),
1138                    name,
1139                    // Markers are read from the original source: the shadow
1140                    // turns `[正常系]` into a bogus `[xxx]`.
1141                    markers: collect_preceding_markers_with(
1142                        &children,
1143                        idx,
1144                        &shadow.shadow,
1145                        &|n| shadow.node_text(n),
1146                        &JVM_COMMENT_KINDS,
1147                        &|n| shadow.node_text(n).is_some_and(is_doc_comment),
1148                    ),
1149                    content: content.to_string(),
1150                });
1151            }
1152            _ => {}
1153        }
1154    }
1155    Ok(())
1156}
1157
1158fn kotlin_is_test_function(func: tree_sitter::Node, shadow: &KotlinShadow) -> bool {
1159    let mut cursor = func.walk();
1160    let Some(modifiers) = func.children(&mut cursor).find(|n| n.kind() == "modifiers") else {
1161        return false;
1162    };
1163    let mut c2 = modifiers.walk();
1164    let found = modifiers
1165        .children(&mut c2)
1166        .filter(|n| n.kind() == "annotation")
1167        .any(|ann| kotlin_annotation_has_test(ann, shadow));
1168    found
1169}
1170
1171/// Whether any `user_type` under the annotation (outside argument lists) ends
1172/// in a test annotation name.
1173fn kotlin_annotation_has_test(node: tree_sitter::Node, shadow: &KotlinShadow) -> bool {
1174    let mut cursor = node.walk();
1175    for child in node.children(&mut cursor) {
1176        match child.kind() {
1177            "user_type" => {
1178                let mut c = child.walk();
1179                let last = child
1180                    .children(&mut c)
1181                    .filter(|n| matches!(n.kind(), "identifier" | "type_identifier"))
1182                    .last();
1183                if let Some(last) = last {
1184                    if jvm_name_is_test(shadow.node_text(last).unwrap_or("")) {
1185                        return true;
1186                    }
1187                }
1188            }
1189            "value_arguments" | "use_site_target" => {}
1190            _ => {
1191                if kotlin_annotation_has_test(child, shadow) {
1192                    return true;
1193                }
1194            }
1195        }
1196    }
1197    false
1198}
1199
1200#[cfg(test)]
1201mod tests {
1202    use super::*;
1203
1204    fn find<'a>(tests: &'a [ExtractedTest], name: &str) -> &'a ExtractedTest {
1205        tests
1206            .iter()
1207            .find(|t| t.name == name)
1208            .unwrap_or_else(|| panic!("test named {name} not found in {tests:?}"))
1209    }
1210
1211    // [FR-012]
1212    #[test]
1213    fn goは直前に隣接するコメントをマーカーとして抽出する() {
1214        let source = r#"package auth
1215
1216// [FR-001]
1217func TestRejectsShortPassword(t *testing.T) {
1218    doSomething()
1219}
1220"#;
1221        let tests = extract_go_tests("internal/auth/login_test.go", source).unwrap();
1222        let t = find(&tests, "TestRejectsShortPassword");
1223        assert_eq!(t.markers, vec!["FR-001".to_string()]);
1224    }
1225
1226    // [FR-012]
1227    #[test]
1228    fn goは空行を挟んだコメントや欠落をマーカーなしとして無視する() {
1229        let source = r#"package auth
1230
1231// [FR-002]
1232
1233func TestWithBlankLine(t *testing.T) {
1234}
1235
1236func TestNoComment(t *testing.T) {
1237}
1238"#;
1239        let tests = extract_go_tests("internal/auth/login_test.go", source).unwrap();
1240        assert!(find(&tests, "TestWithBlankLine").markers.is_empty());
1241        assert!(find(&tests, "TestNoComment").markers.is_empty());
1242    }
1243
1244    // [FR-013]
1245    #[test]
1246    fn goのtest識別子はディレクトリと関数名を組み合わせた形式になる() {
1247        let source = "package auth\n\n// [FR-001]\nfunc TestLogin(t *testing.T) {}\n";
1248        let tests = extract_go_tests("internal/auth/login_test.go", source).unwrap();
1249        assert_eq!(find(&tests, "TestLogin").id, "internal/auth:TestLogin");
1250    }
1251
1252    // [FR-013]
1253    #[test]
1254    fn goのtest識別子はリポジトリ直下ならドットになる() {
1255        let source = "package main\n\n// [FR-001]\nfunc TestRoot(t *testing.T) {}\n";
1256        let tests = extract_go_tests("login_test.go", source).unwrap();
1257        assert_eq!(find(&tests, "TestRoot").id, ".:TestRoot");
1258    }
1259
1260    // [FR-003]
1261    #[test]
1262    fn rustの関数本体の変更はcontentに反映される() {
1263        let base = r#"// [FR-001]
1264#[test]
1265fn rejects_short_password() {
1266    assert!(true);
1267}
1268"#;
1269        let changed = r#"// [FR-001]
1270#[test]
1271fn rejects_short_password() {
1272    assert!(false);
1273}
1274"#;
1275        let a = find(
1276            &extract_rust_tests("src/auth.rs", base, &[]).unwrap(),
1277            "rejects_short_password",
1278        )
1279        .content
1280        .clone();
1281        let b = find(
1282            &extract_rust_tests("src/auth.rs", changed, &[]).unwrap(),
1283            "rejects_short_password",
1284        )
1285        .content
1286        .clone();
1287        assert_ne!(a, b);
1288        assert!(a.contains("fn rejects_short_password"));
1289        assert!(a.contains("assert!(true)"));
1290    }
1291
1292    // [FR-003]
1293    #[test]
1294    fn goの関数本体の変更はcontentに反映される() {
1295        let base = "package auth\n\n// [FR-001]\nfunc TestRejectsShortPassword(t *testing.T) {\n\tassertTrue(t, true)\n}\n";
1296        let changed = "package auth\n\n// [FR-001]\nfunc TestRejectsShortPassword(t *testing.T) {\n\tassertTrue(t, false)\n}\n";
1297        let a = find(
1298            &extract_go_tests("internal/auth/login_test.go", base).unwrap(),
1299            "TestRejectsShortPassword",
1300        )
1301        .content
1302        .clone();
1303        let b = find(
1304            &extract_go_tests("internal/auth/login_test.go", changed).unwrap(),
1305            "TestRejectsShortPassword",
1306        )
1307        .content
1308        .clone();
1309        assert_ne!(a, b);
1310        assert!(a.contains("func TestRejectsShortPassword"));
1311        assert!(a.contains("assertTrue(t, true)"));
1312    }
1313
1314    // [FR-004]
1315    #[test]
1316    fn rustのcontentは外側の属性とマーカーコメントを含まない() {
1317        let source = r#"// [FR-001]
1318#[test]
1319fn rejects_short_password() {
1320    assert!(true);
1321}
1322"#;
1323        let extracted = extract_rust_tests("src/auth.rs", source, &[]).unwrap();
1324        let t = find(&extracted, "rejects_short_password");
1325        assert!(!t.content.contains("#[test]"));
1326        assert!(!t.content.contains("[FR-001]"));
1327        assert!(t.content.contains("fn rejects_short_password"));
1328    }
1329
1330    fn rs(source: &str, extra: &[&str]) -> Vec<ExtractedTest> {
1331        let extra: Vec<String> = extra.iter().map(|s| s.to_string()).collect();
1332        extract_rust_tests("src/auth.rs", source, &extra).unwrap()
1333    }
1334
1335    fn has(tests: &[ExtractedTest], name: &str) -> bool {
1336        tests.iter().any(|t| t.name == name)
1337    }
1338
1339    // [FR-015]
1340    #[test]
1341    fn rustは属性パスの末尾がtestならtestとして抽出する() {
1342        let source = r#"// [FR-001]
1343#[test]
1344fn plain_test() {}
1345
1346// [FR-002]
1347#[tokio::test]
1348async fn async_test() {}
1349
1350// [FR-003]
1351#[async_std::test]
1352async fn async_std_test() {}
1353
1354// [FR-004]
1355#[tokio::test(flavor = "multi_thread")]
1356async fn multi_thread_test() {}
1357
1358// [FR-005]
1359#[ test ]
1360fn spaced_test() {}
1361"#;
1362        let tests = rs(source, &[]);
1363        for name in [
1364            "plain_test",
1365            "async_test",
1366            "async_std_test",
1367            "multi_thread_test",
1368            "spaced_test",
1369        ] {
1370            assert!(has(&tests, name), "{name} should be extracted");
1371        }
1372        assert_eq!(
1373            find(&tests, "async_test").markers,
1374            vec!["FR-002".to_string()]
1375        );
1376    }
1377
1378    // [FR-015]
1379    #[test]
1380    fn rustのrstestは追加指定がなければ対象外で指定があれば対象になる() {
1381        let source = "// [FR-001]\n#[rstest]\nfn t() {}\n";
1382        assert!(!has(&rs(source, &[]), "t"));
1383        assert!(has(&rs(source, &["rstest"]), "t"));
1384
1385        let scoped = "// [FR-001]\n#[rstest::rstest]\nfn t() {}\n";
1386        assert!(!has(&rs(scoped, &[]), "t"));
1387        assert!(has(&rs(scoped, &["rstest"]), "t"));
1388    }
1389
1390    // [FR-015]
1391    #[test]
1392    fn rustのtest_caseとcfg_attrのtestは対象外() {
1393        let source = r#"// [FR-001]
1394#[test_case(1)]
1395fn case_test() {}
1396
1397// [FR-002]
1398#[cfg_attr(x, test)]
1399fn cfg_attr_test() {}
1400"#;
1401        let tests = rs(source, &[]);
1402        assert!(!has(&tests, "case_test"));
1403        assert!(!has(&tests, "cfg_attr_test"));
1404
1405        let tests = rs(source, &["test_case"]);
1406        assert!(has(&tests, "case_test"));
1407        assert!(!has(&tests, "cfg_attr_test"));
1408    }
1409
1410    // [FR-015]
1411    #[test]
1412    fn rustのinner_attributeのtestはtestを作らない() {
1413        let source = "#![test]\n\n// [FR-001]\nfn helper() {}\n";
1414        assert!(rs(source, &[]).is_empty());
1415    }
1416
1417    // [FR-016]
1418    #[test]
1419    fn rustは最初の対象属性の直前のコメントをマーカーにする() {
1420        let source = "// [FR-001]\n#[rstest]\n#[case(1)]\n#[tokio::test]\nfn t() {}\n";
1421        let tests = rs(source, &["rstest"]);
1422        assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
1423    }
1424
1425    // [FR-016]
1426    #[test]
1427    fn rustは対象属性の設定によりマーカー位置が変わる() {
1428        let source = "#[rstest]\n// [FR-001]\n#[tokio::test]\nfn t() {}\n";
1429        let without = rs(source, &[]);
1430        assert_eq!(find(&without, "t").markers, vec!["FR-001".to_string()]);
1431        let with = rs(source, &["rstest"]);
1432        assert!(find(&with, "t").markers.is_empty());
1433    }
1434
1435    // [FR-016]
1436    #[test]
1437    fn rustは属性間や属性末尾のコメントがあってもtestとして検出しマーカーを取る() {
1438        let between = "// [FR-001]\n#[test]\n// note\n#[ignore]\nfn between() {}\n";
1439        let tests = rs(between, &[]);
1440        assert_eq!(find(&tests, "between").markers, vec!["FR-001".to_string()]);
1441
1442        let trailing = "// [FR-001]\n#[test] // trailing\n#[ignore]\nfn trailing() {}\n";
1443        let tests = rs(trailing, &[]);
1444        assert_eq!(find(&tests, "trailing").markers, vec!["FR-001".to_string()]);
1445
1446        let block = "// [FR-001]\n#[test]\n/* block */\n#[ignore]\nfn block() {}\n";
1447        assert_eq!(
1448            find(&rs(block, &[]), "block").markers,
1449            vec!["FR-001".to_string()]
1450        );
1451    }
1452
1453    // [FR-004]
1454    #[test]
1455    fn tokioのtest属性はcontentに含まれない() {
1456        let source = "// [FR-001]\n#[tokio::test]\nasync fn t() { assert!(true); }\n";
1457        let tests = rs(source, &[]);
1458        let t = find(&tests, "t");
1459        assert!(!t.content.contains("tokio::test"));
1460        assert!(!t.content.contains("#["));
1461        assert!(t.content.contains("async fn t"));
1462    }
1463
1464    // [FR-016]
1465    #[test]
1466    fn rustはtest属性の直前に隣接するコメントをマーカーとする() {
1467        let source = r#"// [FR-001]
1468#[test]
1469fn adjacent() {}
1470
1471// [FR-002]
1472
1473#[test]
1474fn blank_line_between() {}
1475
1476#[test]
1477fn no_comment() {}
1478"#;
1479        let tests = extract_rust_tests("src/auth.rs", source, &[]).unwrap();
1480        assert_eq!(find(&tests, "adjacent").markers, vec!["FR-001".to_string()]);
1481        assert!(find(&tests, "blank_line_between").markers.is_empty());
1482        assert!(find(&tests, "no_comment").markers.is_empty());
1483    }
1484
1485    // [FR-017]
1486    #[test]
1487    fn rustのtest識別子はトップレベルなら空モジュールパスになる() {
1488        let source = "// [FR-001]\n#[test]\nfn roundtrip() {}\n";
1489        let tests = extract_rust_tests("src/lock.rs", source, &[]).unwrap();
1490        assert_eq!(find(&tests, "roundtrip").id, "src/lock::roundtrip");
1491    }
1492
1493    // [FR-017]
1494    #[test]
1495    fn rustのtest識別子はinline_modを外側から内側の順に連結する() {
1496        let source = r#"mod tests {
1497    // [FR-001]
1498    #[test]
1499    fn roundtrip() {}
1500}
1501"#;
1502        let tests = extract_rust_tests("src/lock.rs", source, &[]).unwrap();
1503        assert_eq!(find(&tests, "roundtrip").id, "src/lock:tests:roundtrip");
1504    }
1505
1506    // [FR-017]
1507    #[test]
1508    fn rustのtest識別子はネストしたinline_modをスラッシュで連結する() {
1509        let source = r#"mod outer {
1510    mod inner {
1511        // [FR-001]
1512        #[test]
1513        fn t() {}
1514    }
1515}
1516"#;
1517        let tests = extract_rust_tests("src/x.rs", source, &[]).unwrap();
1518        assert_eq!(find(&tests, "t").id, "src/x:outer/inner:t");
1519    }
1520
1521    // [FR-016]
1522    #[test]
1523    fn rustは属性が間に挟まるとtestの直前のコメントをマーカーにしない() {
1524        let source = r#"// [FR-001]
1525#[cfg(feature = "x")]
1526#[test]
1527fn not_adjacent() {}
1528"#;
1529        let tests = extract_rust_tests("src/auth.rs", source, &[]).unwrap();
1530        assert!(find(&tests, "not_adjacent").markers.is_empty());
1531    }
1532
1533    // [FR-016]
1534    #[test]
1535    fn rustは他の属性の後ろでもtestの直前のコメントはマーカーになる() {
1536        let source = r#"#[cfg(feature = "x")]
1537// [FR-001]
1538#[test]
1539fn adjacent_after_other_attr() {}
1540"#;
1541        let tests = extract_rust_tests("src/auth.rs", source, &[]).unwrap();
1542        assert_eq!(
1543            find(&tests, "adjacent_after_other_attr").markers,
1544            vec!["FR-001".to_string()]
1545        );
1546    }
1547
1548    // [FR-016]
1549    #[test]
1550    fn rustはtestとfnの間にコメントがあってもtestとして検出しマーカーを採用する() {
1551        let source = r#"// [FR-001]
1552#[test]
1553// 補足
1554fn t() {}
1555"#;
1556        let tests = extract_rust_tests("src/auth.rs", source, &[]).unwrap();
1557        let found = find(&tests, "t");
1558        assert_eq!(found.markers, vec!["FR-001".to_string()]);
1559        assert!(!found.content.contains("補足"));
1560    }
1561
1562    // ---- C# ----
1563
1564    fn cs(source: &str) -> Vec<ExtractedTest> {
1565        extract_csharp_tests("tests/AuthTests.cs", source).unwrap()
1566    }
1567
1568    fn cs_wrap(method: &str) -> String {
1569        format!("public class AuthTests\n{{\n{method}\n}}\n")
1570    }
1571
1572    // [FR-049]
1573    #[test]
1574    fn csharpはif_endif内のtestメソッドも検出する() {
1575        let source = "public class A {\n#if DEBUG\n    // [FR-001]\n    [Fact]\n    public void T() { }\n#endif\n}\n";
1576        let tests = extract_csharp_tests("tests/A.cs", source).unwrap();
1577        assert_eq!(tests.len(), 1);
1578        assert_eq!(tests[0].id, "tests/A:A:T");
1579        assert_eq!(tests[0].markers, vec!["FR-001".to_string()]);
1580    }
1581
1582    // [FR-049]
1583    #[test]
1584    fn csharpは7種類の属性をそれぞれtestとして検出する() {
1585        for attr in [
1586            "Fact",
1587            "Theory",
1588            "Test",
1589            "TestCase",
1590            "TestCaseSource",
1591            "TestMethod",
1592            "DataTestMethod",
1593        ] {
1594            let source = cs_wrap(&format!("    [{attr}]\n    public void T() {{ }}"));
1595            let tests = cs(&source);
1596            assert_eq!(tests.len(), 1, "attribute {attr} should be detected");
1597            assert_eq!(tests[0].name, "T");
1598        }
1599    }
1600
1601    // [FR-049]
1602    #[test]
1603    fn csharpはattribute接尾辞付きや無関係な属性のみのメソッドを検出しない() {
1604        let source = cs_wrap(
1605            "    [FactAttribute]\n    public void A() { }\n\n    [Obsolete]\n    public void B() { }\n\n    public void C() { }\n\n    [Category(\"x\")]\n    public void D() { }",
1606        );
1607        assert!(cs(&source).is_empty());
1608    }
1609
1610    // [FR-049]
1611    #[test]
1612    fn csharpは引数付きの属性も検出する() {
1613        let source = cs_wrap("    [Fact(Skip = \"x\")]\n    public void T() { }");
1614        assert_eq!(cs(&source).len(), 1);
1615    }
1616
1617    // [FR-049]
1618    #[test]
1619    fn csharpは名前空間修飾された属性も検出する() {
1620        let source = cs_wrap(
1621            "    [Xunit.Fact]\n    public void A() { }\n\n    [global::Xunit.Fact]\n    public void B() { }",
1622        );
1623        let tests = cs(&source);
1624        assert_eq!(tests.len(), 2);
1625        find(&tests, "A");
1626        find(&tests, "B");
1627    }
1628
1629    // [FR-049]
1630    #[test]
1631    fn csharpはtarget指定付きの属性も検出する() {
1632        let source = cs_wrap("    [method: Fact]\n    public void T() { }");
1633        assert_eq!(cs(&source).len(), 1);
1634    }
1635
1636    // [FR-049]
1637    #[test]
1638    fn csharpは同一属性リスト内の他の属性と並んだ属性も検出する() {
1639        let source = cs_wrap("    [Trait(\"a\", \"b\"), Fact]\n    public void T() { }");
1640        assert_eq!(cs(&source).len(), 1);
1641    }
1642
1643    // [FR-049]
1644    #[test]
1645    fn csharpは複数の属性リストの順序に関わらず検出する() {
1646        let source = cs_wrap(
1647            "    [Theory]\n    [InlineData(1)]\n    public void A(int x) { }\n\n    [InlineData(1)]\n    [Theory]\n    public void B(int x) { }",
1648        );
1649        let tests = cs(&source);
1650        assert_eq!(tests.len(), 2);
1651        find(&tests, "A");
1652        find(&tests, "B");
1653    }
1654
1655    // [FR-050]
1656    #[test]
1657    fn csharpは直前に隣接するコメントをマーカーとして抽出し空行を挟むと無視する() {
1658        let source = cs_wrap(
1659            "    // [FR-001]\n    [Fact]\n    public void Adjacent() { }\n\n    // [FR-002]\n\n    [Fact]\n    public void Blank() { }\n\n    [Fact]\n    public void None() { }",
1660        );
1661        let tests = cs(&source);
1662        assert_eq!(find(&tests, "Adjacent").markers, vec!["FR-001".to_string()]);
1663        assert!(find(&tests, "Blank").markers.is_empty());
1664        assert!(find(&tests, "None").markers.is_empty());
1665    }
1666
1667    // [FR-050]
1668    #[test]
1669    fn csharpはドキュメントコメントとブロックコメントもマーカーにできる() {
1670        let source = cs_wrap(
1671            "    /// [FR-001]\n    [Fact]\n    public void Doc() { }\n\n    /* [FR-002] */\n    [Fact]\n    public void Block() { }",
1672        );
1673        let tests = cs(&source);
1674        assert_eq!(find(&tests, "Doc").markers, vec!["FR-001".to_string()]);
1675        assert_eq!(find(&tests, "Block").markers, vec!["FR-002".to_string()]);
1676    }
1677
1678    // [FR-050]
1679    #[test]
1680    fn csharpは属性と署名の間のコメントをマーカーにせずcontentにも含めない() {
1681        let source = cs_wrap(
1682            "    [Fact]\n    // [FR-001]\n    public void T() { }\n\n    [Fact]\n    [Trait(\"a\", \"b\")]\n    // 補足\n    public void U() { }",
1683        );
1684        let tests = cs(&source);
1685        let t = find(&tests, "T");
1686        assert!(t.markers.is_empty());
1687        assert!(!t.content.contains("FR-001"));
1688        let u = find(&tests, "U");
1689        assert!(!u.content.contains("補足"));
1690    }
1691
1692    // [FR-050]
1693    #[test]
1694    fn csharpは属性リストの間のコメントをマーカーにせずcontentにも含めない() {
1695        let source = cs_wrap(
1696            "    [Theory]\n    // [FR-001]\n    [InlineData(1)]\n    public void T(int x) { }",
1697        );
1698        let tests = cs(&source);
1699        let t = find(&tests, "T");
1700        assert!(t.markers.is_empty());
1701        assert!(!t.content.contains("FR-001"));
1702    }
1703
1704    // [FR-051]
1705    #[test]
1706    fn csharpのtest識別子はトップレベルclassならファイルパスと型名とメソッド名になる() {
1707        let source =
1708            "public class AuthTests\n{\n    [Fact]\n    public void RejectsShort() { }\n}\n";
1709        let tests = cs(source);
1710        assert_eq!(tests[0].id, "tests/AuthTests:AuthTests:RejectsShort");
1711    }
1712
1713    // [FR-051]
1714    #[test]
1715    fn csharpのtest識別子はネストした型を外側から内側へスラッシュで連結する() {
1716        let source = "public class Outer\n{\n    public class Inner\n    {\n        [Test]\n        public void T3() { }\n    }\n}\n";
1717        let tests = cs(source);
1718        assert_eq!(tests[0].id, "tests/AuthTests:Outer/Inner:T3");
1719    }
1720
1721    // [FR-051]
1722    #[test]
1723    fn csharpのtest識別子はstruct_record_interfaceの中でも型名を辿る() {
1724        let source = "public struct S\n{\n    [Fact]\n    public void A() { }\n}\n\npublic record R\n{\n    [Fact]\n    public void B() { }\n}\n\npublic interface I\n{\n    [Fact]\n    public void C() { }\n}\n";
1725        let tests = cs(source);
1726        assert_eq!(find(&tests, "A").id, "tests/AuthTests:S:A");
1727        assert_eq!(find(&tests, "B").id, "tests/AuthTests:R:B");
1728        assert_eq!(find(&tests, "C").id, "tests/AuthTests:I:C");
1729    }
1730
1731    // [FR-051]
1732    #[test]
1733    fn csharpのtest識別子はblock_namespaceを含まない() {
1734        let source = "namespace Foo.Bar\n{\n    public class AuthTests\n    {\n        [Fact]\n        public void T() { }\n    }\n}\n";
1735        let tests = cs(source);
1736        assert_eq!(tests[0].id, "tests/AuthTests:AuthTests:T");
1737    }
1738
1739    // [FR-051]
1740    #[test]
1741    fn csharpのtest識別子はfile_scoped_namespaceを含まない() {
1742        let source =
1743            "namespace Foo;\n\npublic class AuthTests\n{\n    [Fact]\n    public void T() { }\n}\n";
1744        let tests = cs(source);
1745        assert_eq!(tests[0].id, "tests/AuthTests:AuthTests:T");
1746    }
1747
1748    // [FR-052]
1749    #[test]
1750    fn csharpのcontentは属性を含まず修飾子から始まる() {
1751        let source = cs_wrap(
1752            "    // [FR-001]\n    [Fact]\n    [Trait(\"a\", \"b\")]\n    public void T() { Assert.True(true); }",
1753        );
1754        let tests = cs(&source);
1755        let c = &tests[0].content;
1756        assert!(c.starts_with("public void T()"), "content = {c:?}");
1757        assert!(c.ends_with('}'));
1758        assert!(!c.contains("Fact"));
1759        assert!(!c.contains("Trait"));
1760        assert!(!c.contains("FR-001"));
1761    }
1762
1763    // [FR-052]
1764    #[test]
1765    fn csharpの本体変更はcontentに反映される() {
1766        let a = cs(&cs_wrap(
1767            "    [Fact]\n    public void T() { Assert.True(true); }",
1768        ));
1769        let b = cs(&cs_wrap(
1770            "    [Fact]\n    public void T() { Assert.True(false); }",
1771        ));
1772        assert_ne!(a[0].content, b[0].content);
1773    }
1774
1775    // [FR-052]
1776    #[test]
1777    fn csharpの属性だけの変更はcontentを変えない() {
1778        let a = cs(&cs_wrap(
1779            "    [Fact]\n    public void T() { Assert.True(true); }",
1780        ));
1781        let b = cs(&cs_wrap(
1782            "    [Fact(Skip = \"x\")]\n    [Trait(\"a\", \"b\")]\n    public void T() { Assert.True(true); }",
1783        ));
1784        assert_eq!(a[0].content, b[0].content);
1785    }
1786
1787    // [FR-014]
1788    #[test]
1789    fn csharpは構文エラーのソースでエラーを返す() {
1790        let source = "public class A {\n    [Fact]\n    public void T( {\n}\n";
1791        assert!(extract_csharp_tests("tests/A.cs", source).is_err());
1792    }
1793
1794    // ---- TypeScript / JavaScript ----
1795
1796    fn ts(source: &str) -> Vec<ExtractedTest> {
1797        extract_typescript_tests("src/a.test.ts", source).unwrap()
1798    }
1799
1800    // [FR-054]
1801    #[test]
1802    fn typescriptはtestとitの呼び出しを検出する() {
1803        let tests = ts("test(\"a\", () => {});\nit(\"b\", () => {});\n");
1804        assert_eq!(tests.len(), 2);
1805        find(&tests, "a");
1806        find(&tests, "b");
1807    }
1808
1809    // [FR-054]
1810    #[test]
1811    fn typescriptは許可された修飾子付きの呼び出しを検出する() {
1812        let source = "it.skip(\"a\", () => {});\ntest.only(\"b\", () => {});\ntest.concurrent.only(\"c\", () => {});\ntest.todo(\"d\", () => {});\n";
1813        let tests = ts(source);
1814        assert_eq!(tests.len(), 4);
1815        for n in ["a", "b", "c", "d"] {
1816            find(&tests, n);
1817        }
1818    }
1819
1820    // [FR-054]
1821    #[test]
1822    fn typescriptはdescribe_skipの中のtestも検出する() {
1823        let tests = ts("describe.skip(\"d\", () => {\n  it(\"a\", () => {});\n});\n");
1824        assert_eq!(tests.len(), 1);
1825        assert_eq!(tests[0].id, "src/a.test:d:a");
1826    }
1827
1828    // [FR-054]
1829    #[test]
1830    fn typescriptはfunction式のコールバックも検出する() {
1831        let source = "describe(\"d\", function () {\n  it(\"a\", function () {});\n});\n";
1832        let tests = ts(source);
1833        assert_eq!(tests.len(), 1);
1834        assert_eq!(tests[0].id, "src/a.test:d:a");
1835    }
1836
1837    // [FR-053]
1838    #[test]
1839    fn typescriptはtsxファイルのjsxを含むソースも解析できる() {
1840        let source = "it(\"renders\", () => {\n  const x = <div className=\"a\">hi</div>;\n  expect(x).toBeTruthy();\n});\n";
1841        let tests = extract_typescript_tests("src/a.test.tsx", source).unwrap();
1842        assert_eq!(tests.len(), 1);
1843        assert_eq!(tests[0].id, "src/a.test::renders");
1844    }
1845
1846    // [FR-054]
1847    #[test]
1848    fn typescriptは許可外の修飾子や呼び出し結果の呼び出しを検出しない() {
1849        let source = r#"test.each([1, 2])("each %i", (n) => {});
1850test.skipIf(c)("skipif", () => {});
1851test.extend({ a: 1 });
1852test.describe("desc", () => {});
1853test.runIf(c)("runif", () => {});
1854it?.skip("optional", () => {});
1855"#;
1856        assert!(ts(source).is_empty());
1857    }
1858
1859    // [FR-054]
1860    #[test]
1861    fn typescriptはexport_defaultやskipif_describeやforループ内のitを検出しない() {
1862        let source = r#"export default describe("d", () => { it("y", () => {}); })
1863describe.skipIf(c)("d", () => { it("x", () => {}); });
1864for (const n of [1]) {
1865  it("loop", () => {});
1866}
1867"#;
1868        assert!(ts(source).is_empty());
1869    }
1870
1871    // [FR-054]
1872    #[test]
1873    fn typescriptは許可された修飾子を検出し走査する() {
1874        let source = r#"test.fails("f", () => {});
1875it.sequential("s", () => {});
1876describe.shuffle("d", () => {
1877  it("inner", () => {});
1878});
1879"#;
1880        let tests = ts(source);
1881        assert_eq!(tests.len(), 3);
1882        find(&tests, "f");
1883        find(&tests, "s");
1884        assert_eq!(find(&tests, "inner").id, "src/a.test:d:inner");
1885    }
1886
1887    // [FR-054]
1888    #[test]
1889    fn typescriptはdescribe_eachの中のtestを検出しない() {
1890        let source = "describe.each([1])(\"d %i\", (n) => {\n  it(\"a\", () => {});\n});\n";
1891        assert!(ts(source).is_empty());
1892    }
1893
1894    // [FR-054]
1895    #[test]
1896    fn typescriptはawaitなど式文直下でない呼び出しを検出しない() {
1897        let source = "await it(\"a\", () => {});\nvoid it(\"b\", () => {});\n";
1898        assert!(ts(source).is_empty());
1899    }
1900
1901    // [FR-054]
1902    #[test]
1903    fn typescriptは第1引数が文字列リテラルでない呼び出しを検出しない() {
1904        let source = "const name = \"x\";\nit(name, () => {});\nit(\"a\" + \"b\", () => {});\n";
1905        assert!(ts(source).is_empty());
1906    }
1907
1908    // [FR-054]
1909    #[test]
1910    fn typescriptはhelper関数やtest本体やif文の中のitを検出しない() {
1911        let source = r#"function helper() {
1912  it("in helper", () => {});
1913}
1914test("outer", () => {
1915  it("in test", () => {});
1916});
1917if (cond) {
1918  it("in if", () => {});
1919}
1920"#;
1921        let tests = ts(source);
1922        assert_eq!(tests.len(), 1);
1923        assert_eq!(tests[0].name, "outer");
1924    }
1925
1926    // [FR-056]
1927    #[test]
1928    fn typescriptは置換なしのテンプレート文字列を対象にし置換ありを無視する() {
1929        let source = "it(`plain`, () => {});\nit(`with ${x}`, () => {});\n";
1930        let tests = ts(source);
1931        assert_eq!(tests.len(), 1);
1932        assert_eq!(tests[0].name, "plain");
1933    }
1934
1935    // [FR-056]
1936    #[test]
1937    fn typescriptは空文字名と改行を含む名前を無視する() {
1938        let source = "it(\"\", () => {});\nit(`line1\nline2`, () => {});\nit(\"ok\", () => {});\n";
1939        let tests = ts(source);
1940        assert_eq!(tests.len(), 1);
1941        assert_eq!(tests[0].name, "ok");
1942    }
1943
1944    // [FR-056]
1945    #[test]
1946    fn typescriptは行区切り文字を含む名前を無視する() {
1947        let source = "it(\"a\u{2028}b\", () => {});\nit(\"ok\", () => {});\n";
1948        let tests = ts(source);
1949        assert_eq!(tests.len(), 1);
1950        assert_eq!(tests[0].name, "ok");
1951    }
1952
1953    // [FR-056]
1954    #[test]
1955    fn typescriptはエスケープを解釈せずソース表記のまま名前にする() {
1956        let source = "it('it\\'s', () => {});\n";
1957        let tests = ts(source);
1958        assert_eq!(tests.len(), 1);
1959        assert_eq!(tests[0].name, "it\\'s");
1960        assert_eq!(tests[0].id, "src/a.test::it\\'s");
1961    }
1962
1963    // [FR-056]
1964    #[test]
1965    fn typescriptのtest識別子はdescribeの外なら空のdescribeパスになる() {
1966        let tests = ts("it(\"top\", () => {});\n");
1967        assert_eq!(tests[0].id, "src/a.test::top");
1968    }
1969
1970    // [FR-056]
1971    #[test]
1972    fn typescriptのtest識別子はdescribeを外側から内側へスラッシュで連結する() {
1973        let source = "describe(\"auth\", () => {\n  it(\"one\", () => {});\n  describe(\"inner\", () => {\n    it(\"two\", () => {});\n  });\n});\n";
1974        let tests = ts(source);
1975        assert_eq!(find(&tests, "one").id, "src/a.test:auth:one");
1976        assert_eq!(find(&tests, "two").id, "src/a.test:auth/inner:two");
1977    }
1978
1979    // [FR-056]
1980    #[test]
1981    fn typescriptのtest識別子は対象拡張子をすべて除去しtestやspecは残す() {
1982        let src = "it(\"t\", () => {});\n";
1983        for (path, id) in [
1984            ("src/a.test.ts", "src/a.test::t"),
1985            ("src/a.spec.tsx", "src/a.spec::t"),
1986            ("src/a.test.mjs", "src/a.test::t"),
1987            ("src/a.spec.cts", "src/a.spec::t"),
1988            ("src/a.test.jsx", "src/a.test::t"),
1989            ("src/a.test.js", "src/a.test::t"),
1990            ("src/a.spec.cjs", "src/a.spec::t"),
1991            ("src/a.test.mts", "src/a.test::t"),
1992        ] {
1993            let tests = extract_typescript_tests(path, src).unwrap();
1994            assert_eq!(tests[0].id, id, "{path}");
1995        }
1996    }
1997
1998    // [FR-056]
1999    #[test]
2000    fn typescriptのtest識別子は対象外の拡張子を除去しない() {
2001        let tests =
2002            extract_typescript_tests("src/a.test.ts.bak", "it(\"t\", () => {});\n").unwrap();
2003        assert_eq!(tests[0].id, "src/a.test.ts.bak::t");
2004    }
2005
2006    // [FR-053]
2007    #[test]
2008    fn typescriptはjs系拡張子のファイルからtestを検出する() {
2009        for path in [
2010            "src/a.test.js",
2011            "src/a.spec.mjs",
2012            "src/a.test.cjs",
2013            "src/a.spec.mts",
2014            "src/a.test.cts",
2015        ] {
2016            let tests = extract_typescript_tests(path, "it(\"t\", () => {});\n").unwrap();
2017            assert_eq!(tests.len(), 1, "{path}");
2018        }
2019    }
2020
2021    // [FR-053]
2022    #[test]
2023    fn typescriptはts系拡張子でtsx文法が拒否する角括弧キャストを解析できる() {
2024        let source = "it(\"t\", () => {\n  const n = <number>x;\n});\n";
2025        for path in ["src/a.test.ts", "src/a.test.mts", "src/a.spec.cts"] {
2026            let tests = extract_typescript_tests(path, source).unwrap();
2027            assert_eq!(tests.len(), 1, "{path}");
2028        }
2029    }
2030
2031    // [FR-053]
2032    #[test]
2033    fn typescriptはjsxとjsファイルのjsxを含むソースも解析できる() {
2034        let source = "it(\"renders\", () => {\n  render(<App />);\n});\n";
2035        for path in ["src/a.test.jsx", "src/a.test.js"] {
2036            let tests = extract_typescript_tests(path, source).unwrap();
2037            assert_eq!(tests.len(), 1, "{path}");
2038            assert_eq!(tests[0].id, "src/a.test::renders", "{path}");
2039        }
2040    }
2041
2042    // [FR-055]
2043    #[test]
2044    fn typescriptは直前に隣接するコメントをマーカーとして抽出し空行を挟むと無視する() {
2045        let source = "// [FR-001]\nit(\"adjacent\", () => {});\n\n// [FR-002]\n\nit(\"blank\", () => {});\n\nit(\"none\", () => {});\n";
2046        let tests = ts(source);
2047        assert_eq!(find(&tests, "adjacent").markers, vec!["FR-001".to_string()]);
2048        assert!(find(&tests, "blank").markers.is_empty());
2049        assert!(find(&tests, "none").markers.is_empty());
2050    }
2051
2052    // [FR-055]
2053    #[test]
2054    fn typescriptはdescribe内のtestでも直前のコメントをマーカーにする() {
2055        let source = "describe(\"d\", () => {\n  // [FR-001]\n  it(\"a\", () => {});\n});\n";
2056        let tests = ts(source);
2057        assert_eq!(tests[0].markers, vec!["FR-001".to_string()]);
2058    }
2059
2060    // [FR-003]
2061    #[test]
2062    fn typescriptのcontentは呼び出し文全体でマーカーコメントを含まない() {
2063        let source = "// [FR-001]\nit(\"a\", () => {\n  expect(1).toBe(1);\n});\n";
2064        let tests = ts(source);
2065        let c = &tests[0].content;
2066        assert!(c.starts_with("it(\"a\""), "content = {c:?}");
2067        assert!(c.ends_with(");"), "content = {c:?}");
2068        assert!(!c.contains("FR-001"));
2069    }
2070
2071    // [FR-003]
2072    #[test]
2073    fn typescriptの本体変更はcontentに反映される() {
2074        let a = ts("it(\"a\", () => { expect(1).toBe(1); });\n");
2075        let b = ts("it(\"a\", () => { expect(1).toBe(2); });\n");
2076        assert_ne!(a[0].content, b[0].content);
2077    }
2078
2079    // [FR-014]
2080    #[test]
2081    fn typescriptは構文エラーのソースでエラーを返す() {
2082        let source = "it(\"a\", () => {\n";
2083        assert!(extract_typescript_tests("src/a.test.ts", source).is_err());
2084    }
2085
2086    // ---- Java ----
2087
2088    fn jv(source: &str) -> Vec<ExtractedTest> {
2089        extract_java_tests("tests/AuthTests.java", source).unwrap()
2090    }
2091
2092    fn jv_wrap(body: &str) -> String {
2093        format!("public class AuthTests {{\n{body}\n}}\n")
2094    }
2095
2096    // [FR-059]
2097    #[test]
2098    fn javaは5種類の注釈をそれぞれtestとして検出する() {
2099        for ann in [
2100            "Test",
2101            "ParameterizedTest",
2102            "RepeatedTest",
2103            "TestFactory",
2104            "TestTemplate",
2105        ] {
2106            let source = jv_wrap(&format!("    @{ann}\n    void t() {{ }}"));
2107            let tests = jv(&source);
2108            assert_eq!(tests.len(), 1, "annotation {ann} should be detected");
2109            assert_eq!(tests[0].name, "t");
2110        }
2111    }
2112
2113    // [FR-059]
2114    #[test]
2115    fn javaは修飾名の注釈と引数付きの注釈も検出する() {
2116        let source = jv_wrap(
2117            "    @org.junit.jupiter.api.Test\n    void a() { }\n\n    @Test(timeout = 1)\n    void b() { }",
2118        );
2119        let tests = jv(&source);
2120        assert_eq!(tests.len(), 2);
2121        find(&tests, "a");
2122        find(&tests, "b");
2123    }
2124
2125    // [FR-059]
2126    #[test]
2127    fn javaは無関係な注釈のみのメソッドを検出しない() {
2128        let source = jv_wrap(
2129            "    @Disabled\n    void a() { }\n\n    void b() { }\n\n    @Tag(\"x\")\n    void c() { }",
2130        );
2131        assert!(jv(&source).is_empty());
2132    }
2133
2134    // [FR-059]
2135    #[test]
2136    fn javaは修飾子の後ろに書かれた注釈も検出する() {
2137        let source = jv_wrap("    public @Test void t() { }");
2138        let tests = jv(&source);
2139        assert_eq!(tests.len(), 1);
2140        assert!(tests[0].content.contains("@Test"), "{:?}", tests[0].content);
2141    }
2142
2143    // [FR-060]
2144    #[test]
2145    fn javaは直前に隣接するコメントをマーカーとして抽出し空行を挟むと無視する() {
2146        let source = jv_wrap(
2147            "    // [FR-001]\n    @Test\n    void adjacent() { }\n\n    // [FR-002]\n\n    @Test\n    void blank() { }\n\n    @Test\n    void none() { }",
2148        );
2149        let tests = jv(&source);
2150        assert_eq!(find(&tests, "adjacent").markers, vec!["FR-001".to_string()]);
2151        assert!(find(&tests, "blank").markers.is_empty());
2152        assert!(find(&tests, "none").markers.is_empty());
2153    }
2154
2155    // [FR-060]
2156    #[test]
2157    fn javaはブロックコメントをマーカーにできる() {
2158        let source = jv_wrap("    /* [FR-002] */\n    @Test\n    void t() { }");
2159        assert_eq!(jv(&source)[0].markers, vec!["FR-002".to_string()]);
2160    }
2161
2162    // [FR-060]
2163    #[test]
2164    fn javaはjavadocコメントをマーカーにしない() {
2165        let source = jv_wrap("    /** [FR-001] */\n    @Test\n    void t() { }");
2166        assert!(jv(&source)[0].markers.is_empty());
2167    }
2168
2169    // [FR-060]
2170    #[test]
2171    fn javaは注釈と署名の間のコメントをマーカーにせずcontentにも含めない() {
2172        let source = jv_wrap("    @Test\n    // [FR-001]\n    // 補足\n    void t() { }");
2173        let tests = jv(&source);
2174        assert!(tests[0].markers.is_empty());
2175        assert!(!tests[0].content.contains("FR-001"));
2176        assert!(!tests[0].content.contains("補足"));
2177    }
2178
2179    // [FR-061]
2180    #[test]
2181    fn javaのtest識別子はclassならファイルパスと型名とメソッド名になる() {
2182        let tests = jv(&jv_wrap("    @Test\n    void rejectsShort() { }"));
2183        assert_eq!(tests[0].id, "tests/AuthTests:AuthTests:rejectsShort");
2184    }
2185
2186    // [FR-061]
2187    #[test]
2188    fn javaのtest識別子はネストした型を外側から内側へスラッシュで連結する() {
2189        let source = "public class Outer {\n    @Nested\n    class Inner {\n        @Test\n        void t() { }\n    }\n}\n";
2190        let tests = jv(source);
2191        assert_eq!(tests[0].id, "tests/AuthTests:Outer/Inner:t");
2192    }
2193
2194    // [FR-061]
2195    #[test]
2196    fn javaのtest識別子はrecord_interface_enumの中でも型名を辿る() {
2197        let source = "record R() {\n    @Test\n    void a() { }\n}\n\ninterface I {\n    @Test\n    default void b() { }\n}\n\nenum E {\n    X, Y;\n\n    @Test\n    void c() { }\n}\n";
2198        let tests = jv(source);
2199        assert_eq!(find(&tests, "a").id, "tests/AuthTests:R:a");
2200        assert_eq!(find(&tests, "b").id, "tests/AuthTests:I:b");
2201        assert_eq!(find(&tests, "c").id, "tests/AuthTests:E:c");
2202    }
2203
2204    // [FR-061]
2205    #[test]
2206    fn javaのtest識別子はpackageを含まない() {
2207        let source =
2208            "package com.example;\n\npublic class AuthTests {\n    @Test\n    void t() { }\n}\n";
2209        assert_eq!(jv(source)[0].id, "tests/AuthTests:AuthTests:t");
2210    }
2211
2212    // [FR-059]
2213    #[test]
2214    fn javaはトップレベルメソッドを対象外にする() {
2215        let source = "@Test\nvoid t() { }\n";
2216        let tests = extract_java_tests("tests/AuthTests.java", source).unwrap();
2217        assert!(tests.is_empty());
2218        // a class alongside is still extracted
2219        let source = "class A {\n    @Test\n    void u() { }\n}\n";
2220        assert_eq!(jv(source).len(), 1);
2221    }
2222
2223    // [FR-059]
2224    #[test]
2225    fn javaは匿名クラスとenum定数本体の中を対象外にする() {
2226        let source = "public class A {\n    void helper() {\n        Runnable r = new Runnable() {\n            @Test\n            public void run() { }\n        };\n    }\n\n    @Test\n    void real() { }\n}\n\nenum E {\n    X {\n        @Test\n        void inConst() { }\n    };\n\n    @Test\n    void outside() { }\n}\n";
2227        let tests = jv(source);
2228        let names: Vec<&str> = tests.iter().map(|t| t.name.as_str()).collect();
2229        assert_eq!(names, vec!["real", "outside"], "{names:?}");
2230    }
2231
2232    // [FR-062]
2233    #[test]
2234    fn javaのcontentは注釈を含まず修飾子から始まる() {
2235        let source = jv_wrap(
2236            "    // [FR-001]\n    @Test\n    @Tag(\"a\")\n    public void t() { assertTrue(true); }",
2237        );
2238        let c = &jv(&source)[0].content;
2239        assert!(c.starts_with("public void t()"), "content = {c:?}");
2240        assert!(c.ends_with('}'));
2241        assert!(!c.contains("@Test"));
2242        assert!(!c.contains("@Tag"));
2243        assert!(!c.contains("FR-001"));
2244    }
2245
2246    // [FR-062]
2247    #[test]
2248    fn javaのcontentは型パラメータ付きでも修飾子から始まる() {
2249        let source = jv_wrap("    @Test\n    static <T> void t() { }");
2250        let c = &jv(&source)[0].content;
2251        assert!(c.starts_with("static <T> void t()"), "content = {c:?}");
2252    }
2253
2254    // [FR-062]
2255    #[test]
2256    fn javaのcontentは修飾子なしなら型パラメータから始まる() {
2257        let source = jv_wrap("    @Test\n    <T> void t() { }");
2258        let c = &jv(&source)[0].content;
2259        assert!(c.starts_with("<T> void t()"), "content = {c:?}");
2260    }
2261
2262    // [FR-003]
2263    #[test]
2264    fn javaの本体変更はcontentに反映される() {
2265        let a = jv(&jv_wrap("    @Test\n    void t() { assertTrue(true); }"));
2266        let b = jv(&jv_wrap("    @Test\n    void t() { assertTrue(false); }"));
2267        assert_ne!(a[0].content, b[0].content);
2268    }
2269
2270    // [FR-062]
2271    #[test]
2272    fn javaの注釈だけの変更はcontentを変えない() {
2273        let a = jv(&jv_wrap("    @Test\n    void t() { assertTrue(true); }"));
2274        let b = jv(&jv_wrap(
2275            "    @Test(timeout = 1)\n    @Tag(\"a\")\n    void t() { assertTrue(true); }",
2276        ));
2277        assert_eq!(a[0].content, b[0].content);
2278    }
2279
2280    // [FR-014]
2281    #[test]
2282    fn javaは構文エラーのソースでエラーを返す() {
2283        let source = "public class A {\n    @Test\n    void t( {\n}\n";
2284        assert!(extract_java_tests("tests/A.java", source).is_err());
2285    }
2286
2287    // ---- Kotlin ----
2288
2289    fn kt(source: &str) -> Vec<ExtractedTest> {
2290        extract_kotlin_tests("tests/AuthTest.kt", source).unwrap()
2291    }
2292
2293    fn kt_wrap(body: &str) -> String {
2294        format!("class AuthTest {{\n{body}\n}}\n")
2295    }
2296
2297    // [FR-059]
2298    #[test]
2299    fn kotlinは5種類の注釈をそれぞれtestとして検出する() {
2300        for ann in [
2301            "Test",
2302            "ParameterizedTest",
2303            "RepeatedTest",
2304            "TestFactory",
2305            "TestTemplate",
2306        ] {
2307            let source = kt_wrap(&format!("    @{ann}\n    fun t() {{ }}"));
2308            let tests = kt(&source);
2309            assert_eq!(tests.len(), 1, "annotation {ann} should be detected");
2310            assert_eq!(tests[0].name, "t");
2311        }
2312    }
2313
2314    // [FR-059]
2315    #[test]
2316    fn kotlinは修飾名と複数注釈リストとuse_site_target付きの注釈も検出する() {
2317        let source = kt_wrap(
2318            "    @org.junit.jupiter.api.Test\n    fun a() { }\n\n    @[Test Tag(\"a\")]\n    fun b() { }\n\n    @get:Test\n    fun c() { }",
2319        );
2320        let tests = kt(&source);
2321        assert_eq!(tests.len(), 3, "{tests:?}");
2322        find(&tests, "a");
2323        find(&tests, "b");
2324        find(&tests, "c");
2325    }
2326
2327    // [FR-059]
2328    #[test]
2329    fn kotlinは無関係な注釈のみの関数を検出しない() {
2330        let source = kt_wrap("    @Disabled\n    fun a() { }\n\n    fun b() { }");
2331        assert!(kt(&source).is_empty());
2332    }
2333
2334    // [FR-060]
2335    #[test]
2336    fn kotlinは直前に隣接するコメントをマーカーとして抽出し空行を挟むと無視する() {
2337        let source = kt_wrap(
2338            "    // [FR-001]\n    @Test\n    fun adjacent() { }\n\n    // [FR-002]\n\n    @Test\n    fun blank() { }\n\n    /* [FR-003] */\n    @Test\n    fun block() { }",
2339        );
2340        let tests = kt(&source);
2341        assert_eq!(find(&tests, "adjacent").markers, vec!["FR-001".to_string()]);
2342        assert!(find(&tests, "blank").markers.is_empty());
2343        assert_eq!(find(&tests, "block").markers, vec!["FR-003".to_string()]);
2344    }
2345
2346    // [FR-060]
2347    #[test]
2348    fn kotlinはkdocコメントをマーカーにしない() {
2349        let source = kt_wrap("    /** [Foo] */\n    @Test\n    fun t() { }");
2350        assert!(kt(&source)[0].markers.is_empty());
2351    }
2352
2353    // [FR-060]
2354    #[test]
2355    fn kotlinは注釈と署名の間のコメントをマーカーにせずcontentにも含めない() {
2356        let source = kt_wrap(
2357            "    @Test // [FR-001]\n    fun a() { }\n\n    @Test\n    // [FR-002]\n    fun b() { }",
2358        );
2359        let tests = kt(&source);
2360        for name in ["a", "b"] {
2361            let t = find(&tests, name);
2362            assert!(t.markers.is_empty(), "{name}: {:?}", t.markers);
2363            assert!(!t.content.contains("FR-00"), "{name}: {:?}", t.content);
2364        }
2365    }
2366
2367    // [FR-061]
2368    #[test]
2369    fn kotlinのバッククォート名はバッククォートを除いた名前になる() {
2370        let source = kt_wrap("    @Test\n    fun `一覧 取得 は 200`() { }");
2371        let tests = kt(&source);
2372        assert_eq!(tests[0].name, "一覧 取得 は 200");
2373        assert_eq!(tests[0].id, "tests/AuthTest:AuthTest:一覧 取得 は 200");
2374    }
2375
2376    // [FR-061]
2377    #[test]
2378    fn kotlinのtest識別子はネストした型を外側から内側へスラッシュで連結する() {
2379        let source = "class AuthTest {\n    @Nested\n    inner class Inner {\n        @Test\n        fun t() { }\n    }\n}\n";
2380        assert_eq!(kt(source)[0].id, "tests/AuthTest:AuthTest/Inner:t");
2381    }
2382
2383    // [FR-061]
2384    #[test]
2385    fn kotlinのtest識別子はobjectとcompanion_objectを辿る() {
2386        let source = "object O {\n    @Test\n    fun a() { }\n}\n\nclass C {\n    companion object {\n        @Test\n        fun b() { }\n    }\n\n    companion object Factory {\n        @Test\n        fun c() { }\n    }\n}\n";
2387        // two companion objects in one class is invalid Kotlin semantically but
2388        // syntactically fine; the grammar does not reject it.
2389        let tests = kt(source);
2390        assert_eq!(find(&tests, "a").id, "tests/AuthTest:O:a");
2391        assert_eq!(find(&tests, "b").id, "tests/AuthTest:C/Companion:b");
2392        assert_eq!(find(&tests, "c").id, "tests/AuthTest:C/Factory:c");
2393    }
2394
2395    // [FR-061]
2396    #[test]
2397    fn kotlinのトップレベル関数は型部分が空の識別子になる() {
2398        let source = "@Test\nfun top() { }\n";
2399        let tests = kt(source);
2400        assert_eq!(tests.len(), 1);
2401        assert_eq!(tests[0].id, "tests/AuthTest::top");
2402    }
2403
2404    // [FR-061]
2405    #[test]
2406    fn kotlinのtest識別子はpackageを含まない() {
2407        let source = "package com.example\n\nclass AuthTest {\n    @Test\n    fun t() { }\n}\n";
2408        assert_eq!(kt(source)[0].id, "tests/AuthTest:AuthTest:t");
2409    }
2410
2411    // [FR-061]
2412    #[test]
2413    fn kotlinはコンストラクタ注釈付きの主コンストラクタを持つクラスでもtestを取れる() {
2414        let source = "@SpringBootTest\nclass X @Autowired constructor(private val a: Int) : Base(a) {\n    @Test\n    fun `日本語名`() { }\n}\n";
2415        let tests = kt(source);
2416        assert_eq!(tests.len(), 1, "{tests:?}");
2417        assert_eq!(tests[0].id, "tests/AuthTest:X:日本語名");
2418    }
2419
2420    // [FR-059]
2421    #[test]
2422    fn kotlinは関数本体内のローカル関数や匿名objectやenum定数本体やgetterを対象外にする() {
2423        let source = "class A {\n    @Test\n    fun real() {\n        @Test\n        fun local() { }\n    }\n\n    val x: Int\n        @Test\n        get() = 1\n\n    val r = object : Runnable {\n        @Test\n        fun inObj() { }\n    }\n}\n\nenum class E {\n    X {\n        @Test\n        fun inConst() { }\n    };\n\n    @Test\n    fun outside() { }\n}\n";
2424        let tests = kt(source);
2425        let names: Vec<&str> = tests.iter().map(|t| t.name.as_str()).collect();
2426        assert_eq!(names, vec!["real", "outside"], "{names:?}");
2427    }
2428
2429    // [FR-063]
2430    #[test]
2431    fn kotlinは漢字の関数名と型名のtestを抽出し識別子とcontentが元の文字を保つ() {
2432        let source = "class AuthTest {\n    @Nested\n    inner class 一覧取得 {\n        @Test\n        fun 取得() { println(\"漢字\") }\n    }\n}\n";
2433        let tests = kt(source);
2434        assert_eq!(tests.len(), 1, "{tests:?}");
2435        assert_eq!(tests[0].id, "tests/AuthTest:AuthTest/一覧取得:取得");
2436        assert_eq!(tests[0].name, "取得");
2437        assert!(tests[0].content.contains("取得"));
2438        assert!(tests[0].content.contains("漢字"));
2439        assert!(!tests[0].content.contains('x'), "{:?}", tests[0].content);
2440    }
2441
2442    // [FR-063]
2443    #[test]
2444    fn kotlinはカタカナとハングルの関数名も抽出する() {
2445        let source = kt_wrap("    @Test\n    fun テスト() { }\n\n    @Test\n    fun 테스트() { }");
2446        let tests = kt(&source);
2447        assert_eq!(tests.len(), 2, "{tests:?}");
2448        find(&tests, "テスト");
2449        find(&tests, "테스트");
2450    }
2451
2452    // [FR-063]
2453    #[test]
2454    fn kotlinは非asciiの文字リテラルを含むファイルも解析できる() {
2455        let source =
2456            kt_wrap("    @Test\n    fun t() {\n        val c = 'あ'\n        val d = '漢'\n    }");
2457        let tests = kt(&source);
2458        assert_eq!(tests.len(), 1);
2459        assert!(tests[0].content.contains("'あ'"));
2460    }
2461
2462    // [FR-063]
2463    #[test]
2464    fn kotlinはbom付きファイルも解析できる() {
2465        let source = format!("\u{feff}{}", kt_wrap("    @Test\n    fun t() { }"));
2466        let tests = kt(&source);
2467        assert_eq!(tests.len(), 1);
2468        assert_eq!(tests[0].name, "t");
2469    }
2470
2471    // [FR-062]
2472    #[test]
2473    fn kotlinのcontentは注釈を含まず修飾子から始まる() {
2474        let source = kt_wrap(
2475            "    // [FR-001]\n    @Test\n    internal suspend fun t() { assertTrue(true) }",
2476        );
2477        let c = &kt(&source)[0].content;
2478        assert!(c.starts_with("internal suspend fun t()"), "content = {c:?}");
2479        assert!(c.ends_with('}'));
2480        assert!(!c.contains("@Test"));
2481        assert!(!c.contains("FR-001"));
2482    }
2483
2484    // [FR-062]
2485    #[test]
2486    fn kotlinのcontentは注釈のみならfunから始まる() {
2487        let source = kt_wrap("    @Test\n    @Tag(\"a\")\n    fun t() { }");
2488        let c = &kt(&source)[0].content;
2489        assert!(c.starts_with("fun t()"), "content = {c:?}");
2490    }
2491
2492    // [FR-062]
2493    #[test]
2494    fn kotlinの修飾子の後ろに書かれた注釈はcontentに含まれる() {
2495        let source = kt_wrap("    internal @Test fun t() { }");
2496        let tests = kt(&source);
2497        assert_eq!(tests.len(), 1);
2498        assert!(
2499            tests[0].content.starts_with("internal"),
2500            "{:?}",
2501            tests[0].content
2502        );
2503        assert!(tests[0].content.contains("@Test"), "{:?}", tests[0].content);
2504    }
2505
2506    // [FR-062]
2507    #[test]
2508    fn kotlinの式本体は終端までcontentに含まれる() {
2509        let source = kt_wrap("    @Test\n    fun t() = assertTrue(computeSomething())");
2510        let c = &kt(&source)[0].content;
2511        assert!(c.ends_with("computeSomething())"), "content = {c:?}");
2512    }
2513
2514    // [FR-003]
2515    #[test]
2516    fn kotlinの本体変更はcontentに反映される() {
2517        let a = kt(&kt_wrap("    @Test\n    fun t() { assertTrue(true) }"));
2518        let b = kt(&kt_wrap("    @Test\n    fun t() { assertTrue(false) }"));
2519        assert_ne!(a[0].content, b[0].content);
2520    }
2521
2522    // [FR-062]
2523    #[test]
2524    fn kotlinの注釈だけの変更はcontentを変えない() {
2525        let a = kt(&kt_wrap("    @Test\n    fun t() { assertTrue(true) }"));
2526        let b = kt(&kt_wrap(
2527            "    @Test\n    @Tag(\"a\")\n    fun t() { assertTrue(true) }",
2528        ));
2529        assert_eq!(a[0].content, b[0].content);
2530    }
2531
2532    // [FR-014]
2533    #[test]
2534    fn kotlinは構文エラーのソースでエラーを返す() {
2535        let source = "class A { fun x() {} }\n";
2536        assert!(extract_kotlin_tests("tests/A.kt", source).is_err());
2537    }
2538
2539    // [FR-060]
2540    #[test]
2541    fn kotlinのマーカーは元のソースから取り非asciiの角括弧は偽マーカーにならない() {
2542        let source = kt_wrap(
2543            "    // [正常系]\n    // [FR-001]\n    @Test\n    fun t() { }\n\n    // [仮] メモ\n    @Test\n    fun u() { }\n\n    // [FR\u{2010}001]\n    @Test\n    fun v() { }",
2544        );
2545        let tests = kt(&source);
2546        assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
2547        assert!(find(&tests, "u").markers.is_empty());
2548        assert!(find(&tests, "v").markers.is_empty());
2549    }
2550
2551    // [FR-060]
2552    #[test]
2553    fn kotlinはdocコメントを挟んでも手前のマーカーが有効になる() {
2554        let source = kt_wrap("    // [FR-001]\n    /** KDoc */\n    @Test\n    fun b() { }");
2555        assert_eq!(kt(&source)[0].markers, vec!["FR-001".to_string()]);
2556    }
2557
2558    // [FR-060]
2559    #[test]
2560    fn javaはdocコメントを挟んでも手前のマーカーが有効になる() {
2561        let source = jv_wrap("    // [FR-001]\n    /** Javadoc */\n    @Test\n    void b() { }");
2562        assert_eq!(jv(&source)[0].markers, vec!["FR-001".to_string()]);
2563    }
2564
2565    // [FR-060]
2566    #[test]
2567    fn javaは注釈同士の間のコメントをマーカーにせずcontentにも含めない() {
2568        let source =
2569            jv_wrap("    @Test\n    // [FR-001]\n    @Tag(\"a\")\n    // 補足\n    void t() { }");
2570        let t = &jv(&source)[0];
2571        assert!(t.markers.is_empty());
2572        assert!(!t.content.contains("FR-001"));
2573        assert!(!t.content.contains("補足"));
2574    }
2575
2576    // [FR-062]
2577    #[test]
2578    fn kotlinは注釈の間と直後のコメントをcontentに含めない() {
2579        let source = kt_wrap(
2580            "    @Test\n    // 間のメモ\n    @Tag(\"a\")\n    // 直後のメモ\n    fun t() { }",
2581        );
2582        let t = &kt(&source)[0];
2583        assert!(!t.content.contains("メモ"), "{:?}", t.content);
2584        assert!(t.content.starts_with("fun t()"), "{:?}", t.content);
2585    }
2586
2587    // [FR-060]
2588    #[test]
2589    fn kotlinは注釈同士の間のコメントをマーカーにしない() {
2590        let source = kt_wrap("    @Test\n    // [FR-001]\n    @Tag(\"a\")\n    fun t() { }");
2591        assert!(kt(&source)[0].markers.is_empty());
2592    }
2593
2594    // [FR-061]
2595    #[test]
2596    fn kotlinはdata_classとinterfaceの直下のtestも辿る() {
2597        let source = "data class D(val a: Int) {\n    @Test\n    fun a() { }\n}\n\ninterface I {\n    @Test\n    fun b() { }\n}\n";
2598        let tests = kt(source);
2599        assert_eq!(find(&tests, "a").id, "tests/AuthTest:D:a");
2600        assert_eq!(find(&tests, "b").id, "tests/AuthTest:I:b");
2601    }
2602
2603    // [FR-061]
2604    #[test]
2605    fn kotlinは制御文字や空を含む名前のtestを無視する() {
2606        let source =
2607            kt_wrap("    @Test\n    fun `tab\there`() { }\n\n    @Test\n    fun `ok name`() { }");
2608        let tests = kt(&source);
2609        assert_eq!(tests.len(), 1, "{tests:?}");
2610        assert_eq!(tests[0].name, "ok name");
2611    }
2612
2613    // [FR-063]
2614    #[test]
2615    fn kotlinは注釈2つで基底型なしのクラス内の注釈付きinner_classでもtestを取れる() {
2616        let source = "@A\n@C\nclass P {\n    @Nested\n    inner class X {\n        // [FR-001]\n        @Test\n        fun t() { }\n    }\n}\n";
2617        let tests = kt(source);
2618        assert_eq!(tests.len(), 1, "{tests:?}");
2619        assert_eq!(tests[0].id, "tests/AuthTest:P/X:t");
2620        assert_eq!(tests[0].markers, vec!["FR-001".to_string()]);
2621    }
2622
2623    // [FR-063]
2624    #[test]
2625    fn kotlinはnestedを含む複数注釈クラスでも非asciiの名前を元のまま取る() {
2626        let source = "@A\n@C\nclass P {\n    @Nested\n    inner class 一覧取得 {\n        @Test\n        fun `取得 する`() { }\n    }\n}\n";
2627        let tests = kt(source);
2628        assert_eq!(tests[0].id, "tests/AuthTest:P/一覧取得:取得 する");
2629    }
2630
2631    // [FR-063]
2632    #[test]
2633    fn kotlinは宣言以外のトップレベル構造やキーワードを含む文字列でもok空になる() {
2634        for source in [
2635            "package a\n// helper for class X\nval x = 1\n",
2636            "package a\nval fixture = object : Runnable {\n    override fun run() {}\n}\n",
2637            "package a\nval f = fun() { }\n",
2638            "package a\nconst val NAME = \"class\"\n",
2639        ] {
2640            let tests = kt(source);
2641            assert!(tests.is_empty(), "{source:?}: {tests:?}");
2642        }
2643    }
2644
2645    // [FR-063]
2646    #[test]
2647    fn kotlinは文法の不具合で宣言が認識されなかった場合に構文エラーを返す() {
2648        let source = "@A\n@C\nclass P {\n    @Nested\n    inner class X {\n    }\n}\n";
2649        assert!(kotlin_extract("tests/P.kt", source, true).is_ok());
2650        assert!(kotlin_extract("tests/P.kt", source, false).is_err());
2651    }
2652
2653    // [FR-063]
2654    #[test]
2655    fn kotlinはshebangやimportやtypealiasなど宣言以外の通常の構造でok空になる() {
2656        let source = "#!/usr/bin/env kotlin\n@file:JvmName(\"X\")\npackage a\n\nimport b.C\n\ntypealias N = String\nval x = 1\nval y: Int\n    get() = 2\n";
2657        assert!(kt(source).is_empty());
2658    }
2659
2660    // ---- コメント隣接判定(FR-016 / FR-064) ----
2661
2662    // [FR-016]
2663    #[test]
2664    fn rustはdocコメントを挟んでもマーカーを取る() {
2665        let tests = rs("// [FR-001]\n/// doc\n#[test]\nfn t() {}\n", &[]);
2666        assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
2667    }
2668
2669    // [FR-016]
2670    #[test]
2671    fn rustの外側docコメントはマーカーにならない() {
2672        let tests = rs("/// [FR-001]\n#[test]\nfn t() {}\n", &[]);
2673        assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2674    }
2675
2676    // [FR-016]
2677    #[test]
2678    fn rustの内側docコメントはマーカーにならない() {
2679        let tests = rs("mod m {\n//! [FR-001]\n#[test]\nfn t() {}\n}\n", &[]);
2680        assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2681    }
2682
2683    // [FR-016]
2684    #[test]
2685    fn rustはブロックdocコメントを挟んでもマーカーを取る() {
2686        let tests = rs("// [FR-001]\n/** doc */\n#[test]\nfn t() {}\n", &[]);
2687        assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
2688    }
2689
2690    // [FR-016]
2691    #[test]
2692    fn rustのスラッシュ4つのコメントは通常コメントだが余分な記号が本文に残りマーカーにならない() {
2693        let tests = rs("//// [FR-001]\n#[test]\nfn t() {}\n", &[]);
2694        assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2695    }
2696
2697    // [FR-016]
2698    #[test]
2699    fn rustはdocコメントの後に空行があるとマーカーを取らない() {
2700        let tests = rs("// [FR-001]\n/// doc\n\n#[test]\nfn t() {}\n", &[]);
2701        assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2702    }
2703
2704    // [FR-016]
2705    #[test]
2706    fn rustはcrlfでもdocコメントを挟んでマーカーを取る() {
2707        let tests = rs("// [FR-001]\r\n/// doc\r\n#[test]\r\nfn t() {}\r\n", &[]);
2708        assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
2709    }
2710
2711    // [FR-064]
2712    #[test]
2713    fn goは前の宣言の末尾行コメントを次のtestのマーカーにしない() {
2714        let source =
2715            "package a\n\nfunc TestA(t *testing.T) {} // [FR-001]\nfunc TestB(t *testing.T) {}\n";
2716        let tests = extract_go_tests("a/x_test.go", source).unwrap();
2717        assert!(find(&tests, "TestB").markers.is_empty());
2718    }
2719
2720    // [FR-064]
2721    #[test]
2722    fn rustは前の宣言の末尾行コメントを次のtestのマーカーにしない() {
2723        let tests = rs("fn x() {} // [FR-001]\n#[test]\nfn t() {}\n", &[]);
2724        assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2725    }
2726
2727    // [FR-064]
2728    #[test]
2729    fn csharpは前の宣言の末尾行コメントを次のtestのマーカーにしない() {
2730        let source =
2731            cs_wrap("    public void X() {} // [FR-001]\n    [Fact]\n    public void T() {}");
2732        let tests = cs(&source);
2733        assert!(find(&tests, "T").markers.is_empty());
2734    }
2735
2736    // [FR-064]
2737    #[test]
2738    fn typescriptは前の文の末尾行コメントを次のtestのマーカーにしない() {
2739        let tests = ts("it(\"a\", () => {}); // [FR-001]\nit(\"b\", () => {});\n");
2740        assert!(find(&tests, "b").markers.is_empty());
2741    }
2742
2743    // [FR-064]
2744    #[test]
2745    fn javaは前の宣言の末尾行コメントを次のtestのマーカーにしない() {
2746        let source = jv_wrap("    void x() {} // [FR-001]\n    @Test\n    void t() {}");
2747        let tests = jv(&source);
2748        assert!(find(&tests, "t").markers.is_empty());
2749    }
2750
2751    // [FR-064]
2752    #[test]
2753    fn kotlinは非ascii識別子の後の末尾行コメントを次のtestのマーカーにしない() {
2754        let source = kt_wrap("    fun `日本語`() {} // [FR-001]\n    @Test\n    fun t() {}");
2755        let tests = kt(&source);
2756        assert!(find(&tests, "t").markers.is_empty());
2757    }
2758
2759    // [FR-064]
2760    #[test]
2761    fn rustはインデント付きの行頭コメントをマーカーにする() {
2762        let tests = rs(
2763            "mod m {\n    // [FR-001]\n    #[test]\n    fn t() {}\n}\n",
2764            &[],
2765        );
2766        assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
2767    }
2768
2769    // [FR-064]
2770    #[test]
2771    fn typescriptは同じ行に複数のコメントがあると2つ目以降は行頭でないので打ち切る() {
2772        let tests = ts("/* [FR-001] */ // note\nit(\"b\", () => {});\n");
2773        assert!(find(&tests, "b").markers.is_empty());
2774    }
2775
2776    // [FR-064]
2777    #[test]
2778    fn rustはbomの直後の1行目マーカーを取る() {
2779        let tests = rs("\u{feff}// [FR-001]\n#[test]\nfn t() {}\n", &[]);
2780        assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
2781    }
2782
2783    // [FR-064]
2784    #[test]
2785    fn goはbomの直後の1行目マーカーを取る() {
2786        let source = "\u{feff}// [FR-001]\nfunc TestA(t *testing.T) {}\n";
2787        let tests = extract_go_tests("a/x_test.go", source).unwrap();
2788        assert_eq!(find(&tests, "TestA").markers, vec!["FR-001".to_string()]);
2789    }
2790
2791    // [FR-016]
2792    #[test]
2793    fn rustの内側ブロックdocコメントはマーカーにならない() {
2794        let tests = rs("mod m {\n/*! [FR-001] */\n#[test]\nfn t() {}\n}\n", &[]);
2795        assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2796    }
2797
2798    // [FR-016]
2799    #[test]
2800    fn rustのスラッシュ星3つのコメントは通常コメントだが余分な記号が本文に残りマーカーにならない() {
2801        let tests = rs("/*** [FR-001] */\n#[test]\nfn t() {}\n", &[]);
2802        assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2803    }
2804
2805    // [FR-016]
2806    #[test]
2807    fn rustの外側ブロックdocコメントはマーカーにならない() {
2808        let tests = rs("/** [FR-001] */\n#[test]\nfn t() {}\n", &[]);
2809        assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2810    }
2811
2812    // ---- マーカーの厳格化(FR-121) ----
2813
2814    /// (コメント, 期待するマーカー)。コメントの直後に test が続く。
2815    fn marker_cases() -> Vec<(&'static str, Vec<&'static str>)> {
2816        vec![
2817            ("// [FR-001]", vec!["FR-001"]),
2818            ("//   [FR-001]   ", vec!["FR-001"]),
2819            ("// [FR-001] [FR-002]", vec!["FR-001", "FR-002"]),
2820            ("// [FR-001]\n// [FR-002]", vec!["FR-001", "FR-002"]),
2821            ("//// [FR-001]", vec![]),
2822            ("/*** [FR-001] */", vec![]),
2823            ("// [FR-001] 説明", vec![]),
2824            ("// 説明 [FR-001]", vec![]),
2825            ("// 説明 [0] 説明", vec![]),
2826            ("// [FR-001] [正常系]", vec![]),
2827            ("// [FR-001][FR-002]", vec![]),
2828            ("//", vec![]),
2829            ("/* [FR-001] */", vec!["FR-001"]),
2830            ("/*\n * [FR-001]\n */", vec!["FR-001"]),
2831            (
2832                "/*\n * [FR-001]\n * [FR-002]\n */",
2833                vec!["FR-001", "FR-002"],
2834            ),
2835            ("/* [FR-001] [FR-002] */", vec!["FR-001", "FR-002"]),
2836            ("/*\n * [FR-001] 説明\n */", vec![]),
2837            ("/* 説明 [0] */", vec![]),
2838        ]
2839    }
2840
2841    fn assert_marker_cases(markers_of: impl Fn(&str) -> Vec<String>) {
2842        for (comment, expected) in marker_cases() {
2843            assert_eq!(markers_of(comment), expected, "comment: {comment:?}");
2844        }
2845    }
2846
2847    // [FR-121]
2848    #[test]
2849    fn goはコメントが単一トークンだけのときマーカーにする() {
2850        assert_marker_cases(|c| {
2851            let source = format!("{c}\nfunc TestA(t *testing.T) {{}}\n");
2852            find(&extract_go_tests("a/x_test.go", &source).unwrap(), "TestA")
2853                .markers
2854                .clone()
2855        });
2856    }
2857
2858    // [FR-121]
2859    #[test]
2860    fn rustはコメントが単一トークンだけのときマーカーにする() {
2861        assert_marker_cases(|c| {
2862            rs(&format!("{c}\n#[test]\nfn t() {{}}\n"), &[])[0]
2863                .markers
2864                .clone()
2865        });
2866    }
2867
2868    // [FR-121]
2869    #[test]
2870    fn csharpはコメントが単一トークンだけのときマーカーにする() {
2871        assert_marker_cases(|c| {
2872            cs(&cs_wrap(&format!("{c}\n[Fact]\npublic void T() {{ }}")))[0]
2873                .markers
2874                .clone()
2875        });
2876    }
2877
2878    // [FR-121]
2879    #[test]
2880    fn javaはコメントが単一トークンだけのときマーカーにする() {
2881        assert_marker_cases(|c| {
2882            jv(&jv_wrap(&format!("{c}\n@Test\nvoid t() {{ }}")))[0]
2883                .markers
2884                .clone()
2885        });
2886    }
2887
2888    // [FR-121]
2889    #[test]
2890    fn kotlinはコメントが単一トークンだけのときマーカーにする() {
2891        assert_marker_cases(|c| {
2892            kt(&kt_wrap(&format!("{c}\n@Test\nfun t() {{ }}")))[0]
2893                .markers
2894                .clone()
2895        });
2896    }
2897
2898    // [FR-121]
2899    #[test]
2900    fn typescriptはコメントが単一トークンだけのときマーカーにする() {
2901        assert_marker_cases(|c| {
2902            ts(&format!("{c}\nit(\"t\", () => {{}});\n"))[0]
2903                .markers
2904                .clone()
2905        });
2906    }
2907
2908    // [FR-121]
2909    #[test]
2910    fn csharpの三連スラッシュコメントも単一トークンだけならマーカーにする() {
2911        let tests = cs(&cs_wrap("/// [FR-001]\n[Fact]\npublic void T() { }"));
2912        assert_eq!(tests[0].markers, vec!["FR-001".to_string()]);
2913        let tests = cs(&cs_wrap("/// [FR-001] 説明\n[Fact]\npublic void T() { }"));
2914        assert!(tests[0].markers.is_empty());
2915    }
2916
2917    // ---- Kotlin の位置引数付き注釈(FR-063) ----
2918
2919    const KOTLIN_KDOC_FIXTURE: &str = include_str!("../fixtures/kotlin_kdoc_annotation.kt");
2920    const KOTLIN_SUPPORT_FIXTURE: &str = include_str!("../fixtures/kotlin_annotation_support.kt");
2921
2922    // [FR-063]
2923    #[test]
2924    fn kotlinは直前の宣言とコメントの後の位置引数付き注釈でも解析できる() {
2925        let tests = kt("fun g() {}\n// a\n@S(\"L\")\nfun f() {}\n");
2926        assert!(tests.is_empty(), "{tests:?}");
2927    }
2928
2929    // [FR-063]
2930    #[test]
2931    fn kotlinは非asciiのkdocと位置引数付き注釈の組み合わせでも解析できる() {
2932        let tests = kt(KOTLIN_KDOC_FIXTURE);
2933        assert!(tests.is_empty(), "{tests:?}");
2934    }
2935
2936    // [FR-063]
2937    #[test]
2938    fn kotlinは位置引数付き注釈を持つサポートコードのファイルを解析できる() {
2939        let tests = kt(KOTLIN_SUPPORT_FIXTURE);
2940        assert!(tests.is_empty(), "{tests:?}");
2941    }
2942
2943    // [FR-063]
2944    #[test]
2945    fn kotlinは位置引数付き注釈の後ろにtestが続いてもtestを取る() {
2946        let tests = kt("fun g(){}\n// a\n@DisplayName(\"x\")\n@Test\nfun f(){}\n");
2947        assert_eq!(tests.len(), 1, "{tests:?}");
2948        assert_eq!(tests[0].name, "f");
2949    }
2950
2951    // [FR-063]
2952    #[test]
2953    fn kotlinは位置引数付き注釈の後ろのtestのマーカーも取る() {
2954        let tests = kt("fun g(){}\n// a\n// [FR-001]\n@DisplayName(\"x\")\n@Test\nfun f(){}\n");
2955        assert_eq!(tests.len(), 1, "{tests:?}");
2956        assert_eq!(tests[0].markers, vec!["FR-001".to_string()]);
2957    }
2958
2959    // [FR-014]
2960    #[test]
2961    fn kotlinは位置引数付き注釈のある本当の構文エラーを引き続きエラーにする() {
2962        let source = "fun g() {}\n// a\n@S(\"L\")\nfun f( {\n";
2963        assert!(extract_kotlin_tests("tests/A.kt", source).is_err());
2964    }
2965
2966    // [FR-063]
2967    #[test]
2968    fn kotlinは位置引数付き注釈が複数あってもtestを取る() {
2969        let tests = kt("fun g(){}\n// a\n@A(\"x\")\n@B(\"y\")\n@Test\nfun f(){}\n");
2970        assert_eq!(tests.len(), 1, "{tests:?}");
2971    }
2972}