Skip to main content

supercov_engine/
source_units.rs

1//! Declaration units and comment-blind digests of one source file.
2//!
3//! An assertion flow rests on specific code, not on the bytes of a file, and a
4//! test executes specific declarations, not a file. This module names what a
5//! file declares -- functions, methods, classes, in a tree -- and digests each
6//! declaration with comments blanked and nested declarations replaced by a
7//! placeholder, so an edit is attributed to the declaration it lands in and to
8//! nothing else. A test's recorded execution is expressed in the same units.
9//!
10//! Comments are erased, each with the formatting that was its own: the line it
11//! occupied, or the spaces that set it off from code. Blank lines and trailing
12//! whitespace go too, outside string literals, where no language reads them.
13//! Whitespace that belongs to code is kept exactly: indentation is syntax in
14//! Python, and a line break decides a statement in JavaScript, Go, Ruby and
15//! Kotlin. A comment the language or a tool reads -- `//go:embed`, a Ruby
16//! magic comment, a Rust doctest -- is kept as well; erasing it would hide a
17//! change that runs.
18//!
19//! Nothing here is a dependency analysis. The tree says where an edit landed;
20//! whether that edit matters to a test is decided by what the test executed.
21
22use serde::{Deserialize, Serialize};
23use sha2::{Digest, Sha256};
24use std::collections::BTreeMap;
25
26/// One declaration, positioned the way anchors are: one-based lines and
27/// one-based UTF-8 byte columns, end exclusive.
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "camelCase", deny_unknown_fields)]
30pub struct Unit {
31    /// Name path from the file's top level -- `Server.start`, `tests::it_works`
32    /// -- and empty for the file itself. Two declarations that would share a
33    /// path are told apart by `#2`, `#3` in source order.
34    pub path: String,
35    pub kind: String,
36    pub line: usize,
37    pub column: usize,
38    pub end_line: usize,
39    pub end_column: usize,
40    /// Digest of this declaration's own text: comments blanked, each nested
41    /// declaration reduced to one placeholder character.
42    pub digest: String,
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    pub parent: Option<usize>,
45    /// A declaration with no runtime presence: a TypeScript interface, type
46    /// alias, overload signature or ambient declaration. It has a digest, so a
47    /// report can name it, but it takes no part in what a program does.
48    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
49    pub inert: bool,
50}
51
52/// What a parser could say about one file.
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(rename_all = "camelCase", deny_unknown_fields)]
55pub struct Code {
56    /// Digest of everything a program can observe: the whole file with
57    /// comments blanked and inert declarations removed.
58    pub semantic: String,
59    /// Digest of the set of declarations -- kind and path -- that can take
60    /// part in name resolution or dispatch. Adding, removing or renaming one
61    /// changes it; editing a body does not.
62    pub structure: String,
63    /// In source order; `units[0]` is the file itself and contains everything.
64    pub units: Vec<Unit>,
65    /// Lines (one-based, ascending) that hold nothing a program can observe: a
66    /// comment on its own, or blank. Counting lines without them gives a
67    /// position that inserting either does not move.
68    #[serde(default, skip_serializing_if = "Vec::is_empty")]
69    pub erased: Vec<usize>,
70}
71
72impl Unit {
73    /// A unit that can only be used by running it. A class, object, type or
74    /// module is also data -- a field, a constant, a shape -- that code reads
75    /// without a probe firing inside it, so those are never excluded from what
76    /// a test depends on.
77    pub fn is_code(&self) -> bool {
78        matches!(
79            self.kind.as_str(),
80            "function" | "method" | "constructor" | "get" | "set" | "init"
81        )
82    }
83    pub fn contains(&self, line: usize, column: usize) -> bool {
84        (self.line, self.column) <= (line, column)
85            && (line, column) < (self.end_line, self.end_column)
86    }
87    /// `path (line N)`, or `top level` for the file unit.
88    pub fn describe(&self) -> String {
89        if self.path.is_empty() {
90            "top level".to_owned()
91        } else {
92            format!("{} (line {})", self.path, self.line)
93        }
94    }
95}
96
97impl Code {
98    /// A line's number counting only lines that hold code.
99    pub fn code_line(&self, line: usize) -> usize {
100        line - self.erased.partition_point(|e| *e < line)
101    }
102    /// The innermost unit holding a position; the file unit when nothing
103    /// narrower does.
104    pub fn unit_at(&self, line: usize, column: usize) -> usize {
105        // Units are in source order with a parent before its children, so the
106        // last one starting at or before the position is either the innermost
107        // holder or a sibling that ended earlier; its ancestors settle which.
108        let mut index = self
109            .units
110            .partition_point(|u| (u.line, u.column) <= (line, column))
111            .saturating_sub(1);
112        while !self.units[index].contains(line, column) {
113            match self.units[index].parent {
114                Some(parent) => index = parent,
115                None => return 0,
116            }
117        }
118        index
119    }
120    /// A unit and everything it sits inside, innermost first, ending at the
121    /// file unit.
122    pub fn ancestors(&self, index: usize) -> impl Iterator<Item = usize> + '_ {
123        std::iter::successors(Some(index), move |i| self.units[*i].parent)
124    }
125    /// Units by path, for matching one capture against another.
126    pub fn by_path(&self) -> BTreeMap<&str, &Unit> {
127        self.units.iter().map(|u| (u.path.as_str(), u)).collect()
128    }
129    /// What moved between two views of the same file, by declaration path.
130    pub fn diff(&self, new: &Code) -> Diff {
131        let before = self.by_path();
132        let after = new.by_path();
133        Diff {
134            changed: self
135                .units
136                .iter()
137                .enumerate()
138                .filter(|(_, u)| {
139                    after
140                        .get(u.path.as_str())
141                        .is_some_and(|n| n.digest != u.digest)
142                })
143                .map(|(i, _)| i)
144                .collect(),
145            removed: self
146                .units
147                .iter()
148                .enumerate()
149                .filter(|(_, u)| !after.contains_key(u.path.as_str()))
150                .map(|(i, _)| i)
151                .collect(),
152            added: new
153                .units
154                .iter()
155                .enumerate()
156                .filter(|(_, u)| !before.contains_key(u.path.as_str()))
157                .map(|(i, _)| i)
158                .collect(),
159            structural: self.structure != new.structure,
160        }
161    }
162}
163
164/// Indices into the old view for what changed or went, into the new for what
165/// arrived.
166#[derive(Debug, Clone, PartialEq, Eq)]
167pub struct Diff {
168    pub changed: Vec<usize>,
169    pub removed: Vec<usize>,
170    pub added: Vec<usize>,
171    pub structural: bool,
172}
173impl Diff {
174    /// Nothing but declaration bodies changed, and only in units that are
175    /// code with a probe of their own -- the change can reach a test only by
176    /// being run, so a test that ran none of it is untouched.
177    pub fn narrow(&self, old: &Code, probed: &[usize]) -> bool {
178        !self.structural
179            && self.added.is_empty()
180            && self.removed.is_empty()
181            && self
182                .changed
183                .iter()
184                .all(|i| old.units[*i].is_code() && probed.contains(i))
185    }
186}
187
188/// A few names, and how many more there are.
189pub fn named<'u>(units: impl IntoIterator<Item = &'u Unit>) -> String {
190    let names = units.into_iter().map(Unit::describe).collect::<Vec<_>>();
191    match names.len() {
192        0 => "nothing".to_owned(),
193        1..=4 => names.join(", "),
194        n => format!("{}, and {} more", names[..3].join(", "), n - 3),
195    }
196}
197
198/// One declaration the parser found, in bytes.
199struct Item {
200    name: String,
201    kind: &'static str,
202    start: usize,
203    end: usize,
204    inert: bool,
205}
206struct Outline {
207    comments: Vec<(usize, usize)>,
208    /// String literals: whitespace inside them is content, not formatting.
209    strings: Vec<(usize, usize)>,
210    declarations: Vec<Item>,
211    separator: &'static str,
212}
213
214/// The parser's view of a file, or `None` when Supercov has no parser for it
215/// or the file does not parse. A file without a view is compared by its bytes.
216pub fn code(path: &str, source: &str) -> Option<Code> {
217    let extension = path.rsplit_once('.').map_or("", |(_, e)| e);
218    let outline = match extension {
219        "js" | "mjs" | "cjs" | "jsx" | "ts" | "mts" | "cts" | "tsx" => javascript(path, source),
220        "py" => python(source),
221        "rs" => rust(source),
222        "rb" => ruby(source),
223        "go" => go(source),
224        "java" => java(source),
225        "kt" | "kts" => kotlin(source),
226        _ => return None,
227    }?;
228    Some(build(source, outline))
229}
230
231fn short_digest(text: &str) -> String {
232    let digest = Sha256::digest(text.as_bytes());
233    digest[..16].iter().map(|b| format!("{b:02x}")).collect()
234}
235
236/// Comments any language keeps: Supercov's own pragmas.
237fn universally_significant(text: &str) -> bool {
238    text.to_ascii_lowercase().contains("supercov")
239}
240
241/// What to take out for one comment so that the text reads as if the comment
242/// had never been written: the whole line when the comment owns it, the
243/// whitespace that set it off from code otherwise, and a single space where
244/// removing it would glue two words together.
245fn erasure(source: &str, start: usize, end: usize) -> (usize, usize, &'static str) {
246    let bytes = source.as_bytes();
247    let blank = |b: u8| matches!(b, b' ' | b'\t' | b'\r');
248    let word = |b: u8| b.is_ascii_alphanumeric() || b == b'_' || b == b'$';
249    let line_start = source[..start].rfind('\n').map_or(0, |i| i + 1);
250    let mut after = end;
251    while after < bytes.len() && blank(bytes[after]) {
252        after += 1;
253    }
254    let terminated = after >= bytes.len() || bytes[after] == b'\n';
255    if terminated {
256        if bytes[line_start..start].iter().all(|b| blank(*b)) {
257            let stop = if after < bytes.len() {
258                after + 1
259            } else {
260                after
261            };
262            return (line_start, stop, "");
263        }
264        let mut before = start;
265        while before > line_start && blank(bytes[before - 1]) {
266            before -= 1;
267        }
268        return (before, after, "");
269    }
270    let preceded = start == line_start || blank(bytes[start - 1]);
271    let stop = if preceded { after } else { end };
272    let glue = start > 0 && stop < bytes.len() && word(bytes[start - 1]) && word(bytes[stop]);
273    (start, stop, if glue { " " } else { "" })
274}
275
276fn build(source: &str, outline: Outline) -> Code {
277    // The file with insignificant comments erased, and a map from every
278    // original offset to its place in that text. Declaration boundaries never
279    // fall inside a comment, so mapping them is exact.
280    let mut comments = outline.comments;
281    comments.sort_unstable();
282    let mut stripped = String::with_capacity(source.len());
283    let mut map = vec![0usize; source.len() + 1];
284    let mut cursor = 0;
285    for (start, end) in comments {
286        if end > source.len() || start >= end {
287            continue;
288        }
289        let (start, stop, replacement) = erasure(source, start, end);
290        let start = start.max(cursor);
291        if stop <= start {
292            continue;
293        }
294        stripped.push_str(&source[cursor..start]);
295        let base = stripped.len() - (start - cursor);
296        for (i, slot) in map[cursor..start].iter_mut().enumerate() {
297            *slot = base + i;
298        }
299        let here = stripped.len();
300        stripped.push_str(replacement);
301        map[start..stop].fill(here);
302        cursor = stop;
303    }
304    stripped.push_str(&source[cursor..]);
305    let base = stripped.len() - (source.len() - cursor);
306    for (i, slot) in map[cursor..=source.len()].iter_mut().enumerate() {
307        *slot = base + i;
308    }
309    // Then blank lines and trailing whitespace, outside string literals.
310    let mut strings = outline
311        .strings
312        .iter()
313        .filter(|(start, end)| start < end && *end <= source.len())
314        .map(|(start, end)| (map[*start], map[*end]))
315        .collect::<Vec<_>>();
316    strings.sort_unstable();
317    let (stripped, remap) = erase_blank(&stripped, &strings);
318    for slot in &mut map {
319        *slot = remap[*slot];
320    }
321
322    let mut declarations = outline.declarations;
323    declarations.retain(|d| d.start < d.end && d.end <= source.len());
324    declarations.sort_by(|a, b| a.start.cmp(&b.start).then(b.end.cmp(&a.end)));
325    declarations
326        .dedup_by(|later, earlier| later.start == earlier.start && later.end == earlier.end);
327
328    let lines = line_starts(source);
329    let position = |offset: usize| -> (usize, usize) {
330        let line = lines.partition_point(|s| *s <= offset);
331        (line, offset - lines[line - 1] + 1)
332    };
333    // A line nothing of which survived erasure.
334    let erased = (0..lines.len())
335        .filter(|i| {
336            let start = lines[*i];
337            let next = lines.get(i + 1).copied().unwrap_or(source.len());
338            start < next && map[start] == map[next]
339        })
340        .map(|i| i + 1)
341        .collect::<Vec<_>>();
342
343    struct Built {
344        path: String,
345        kind: &'static str,
346        start: usize,
347        end: usize,
348        parent: Option<usize>,
349        inert: bool,
350        children: Vec<usize>,
351    }
352    let mut built = vec![Built {
353        path: String::new(),
354        kind: "file",
355        start: 0,
356        end: source.len(),
357        parent: None,
358        inert: false,
359        children: vec![],
360    }];
361    let mut stack = vec![0usize];
362    let mut names: BTreeMap<(usize, String), usize> = BTreeMap::new();
363    for d in declarations {
364        while stack.len() > 1 && built[*stack.last().unwrap()].end <= d.start {
365            stack.pop();
366        }
367        let parent = *stack.last().unwrap();
368        let index = built.len();
369        let seen = names.entry((parent, d.name.clone())).or_insert(0);
370        *seen += 1;
371        let name = if *seen == 1 {
372            d.name
373        } else {
374            format!("{}#{}", d.name, *seen)
375        };
376        let path = if built[parent].path.is_empty() {
377            name
378        } else {
379            format!("{}{}{}", built[parent].path, outline.separator, name)
380        };
381        built.push(Built {
382            path,
383            kind: d.kind,
384            start: d.start,
385            end: d.end.min(built[parent].end),
386            parent: Some(parent),
387            inert: d.inert || built[parent].inert,
388            children: vec![],
389        });
390        built[parent].children.push(index);
391        stack.push(index);
392    }
393
394    let mut units = Vec::with_capacity(built.len());
395    for unit in &built {
396        let mut text = String::new();
397        let mut cursor = map[unit.start];
398        for child in unit.children.iter().map(|c| &built[*c]) {
399            let (start, end) = (map[child.start], map[child.end]);
400            if start < cursor {
401                continue;
402            }
403            text.push_str(&stripped[cursor..start]);
404            // An inert declaration leaves no mark: adding a type alias next to
405            // a function does not change what the function's file does.
406            if child.inert {
407                cursor = line_end_after(&stripped, end);
408            } else {
409                text.push('\u{0}');
410                cursor = end;
411            }
412        }
413        text.push_str(&stripped[cursor..map[unit.end]]);
414        let (line, column) = position(unit.start);
415        let (end_line, end_column) = position(unit.end);
416        units.push(Unit {
417            path: unit.path.clone(),
418            kind: unit.kind.to_owned(),
419            line,
420            column,
421            end_line,
422            end_column,
423            digest: short_digest(&text),
424            parent: unit.parent,
425            inert: unit.inert,
426        });
427    }
428
429    let mut semantic = String::with_capacity(stripped.len());
430    let mut cursor = 0;
431    for unit in built.iter().filter(|u| u.inert) {
432        let (start, end) = (map[unit.start], map[unit.end]);
433        if start < cursor {
434            continue;
435        }
436        semantic.push_str(&stripped[cursor..start]);
437        cursor = line_end_after(&stripped, end);
438    }
439    semantic.push_str(&stripped[cursor..]);
440
441    let mut structure = built
442        .iter()
443        .skip(1)
444        .filter(|u| !u.inert)
445        .map(|u| format!("{}\u{1}{}\n", u.kind, u.path))
446        .collect::<Vec<_>>();
447    structure.sort_unstable();
448
449    Code {
450        semantic: short_digest(&semantic),
451        structure: short_digest(&structure.concat()),
452        units,
453        erased,
454    }
455}
456
457/// The end of the line a removed declaration sat on, when nothing but
458/// whitespace follows it there; otherwise the position itself.
459fn line_end_after(text: &str, position: usize) -> usize {
460    let bytes = text.as_bytes();
461    let mut p = position;
462    while p < bytes.len() && matches!(bytes[p], b' ' | b'\t' | b'\r') {
463        p += 1;
464    }
465    if p < bytes.len() && bytes[p] == b'\n' {
466        p + 1
467    } else {
468        position
469    }
470}
471
472/// Blank lines and trailing whitespace taken out of `text`, except inside the
473/// given (sorted) string ranges, with a map from every offset of `text` to its
474/// place in the result.
475fn erase_blank(text: &str, strings: &[(usize, usize)]) -> (String, Vec<usize>) {
476    let bytes = text.as_bytes();
477    let blank = |b: u8| matches!(b, b' ' | b'\t' | b'\r');
478    let inside = |start: usize, end: usize| {
479        let i = strings.partition_point(|(_, e)| *e <= start);
480        strings.get(i).is_some_and(|(s, _)| *s < end)
481    };
482    let mut out = String::with_capacity(text.len());
483    let mut map = vec![0usize; text.len() + 1];
484    let mut cursor = 0;
485    let mut line_start = 0;
486    while line_start <= bytes.len() {
487        let line_end = text[line_start..]
488            .find('\n')
489            .map_or(bytes.len(), |i| line_start + i);
490        let next = if line_end < bytes.len() {
491            line_end + 1
492        } else {
493            bytes.len()
494        };
495        let content_end = {
496            let mut e = line_end;
497            while e > line_start && blank(bytes[e - 1]) {
498                e -= 1;
499            }
500            e
501        };
502        // What to drop: the whole line when it is blank, its trailing
503        // whitespace otherwise -- unless a string literal owns that stretch.
504        let (drop_start, drop_end) = if content_end == line_start {
505            (line_start, next)
506        } else {
507            (content_end, line_end)
508        };
509        let dropping = drop_start < drop_end && !inside(drop_start, drop_end);
510        let keep_until = if dropping { drop_start } else { next };
511        out.push_str(&text[cursor..keep_until]);
512        let base = out.len() - (keep_until - cursor);
513        for (i, slot) in map[cursor..keep_until].iter_mut().enumerate() {
514            *slot = base + i;
515        }
516        if dropping {
517            map[drop_start..drop_end].fill(out.len());
518            if drop_end < next {
519                // A trailing-whitespace drop keeps the newline.
520                out.push_str(&text[drop_end..next]);
521                let base = out.len() - (next - drop_end);
522                for (i, slot) in map[drop_end..next].iter_mut().enumerate() {
523                    *slot = base + i;
524                }
525            }
526        }
527        cursor = next;
528        if next == bytes.len() {
529            break;
530        }
531        line_start = next;
532    }
533    map[text.len()] = out.len();
534    (out, map)
535}
536
537fn line_starts(source: &str) -> Vec<usize> {
538    std::iter::once(0)
539        .chain(source.match_indices('\n').map(|(i, _)| i + 1))
540        .collect()
541}
542
543fn collapse_whitespace(text: &str) -> String {
544    text.split_whitespace().collect::<Vec<_>>().join(" ")
545}
546
547// ---------------------------------------------------------------- JavaScript
548
549fn javascript(path: &str, source: &str) -> Option<Outline> {
550    use oxc_ast::ast::*;
551    use oxc_ast_visit::{Visit, walk};
552    use oxc_span::GetSpan;
553
554    let source_type =
555        crate::js_instrumenter::project_source_type(std::path::Path::new(path)).ok()?;
556    let allocator = oxc_allocator::Allocator::default();
557    let parsed = oxc_parser::Parser::new(&allocator, source, source_type).parse();
558    if parsed.panicked || !parsed.errors.is_empty() {
559        return None;
560    }
561    let comments = parsed
562        .program
563        .comments
564        .iter()
565        .map(|c| (c.span.start as usize, c.span.end as usize))
566        .filter(|(start, end)| !universally_significant(&source[*start..*end]))
567        .collect();
568
569    struct Collector<'s> {
570        source: &'s str,
571        declarations: Vec<Item>,
572        strings: Vec<(usize, usize)>,
573    }
574    impl<'s> Collector<'s> {
575        fn record(&mut self, name: String, kind: &'static str, start: u32, end: u32, inert: bool) {
576            self.declarations.push(Item {
577                name,
578                kind,
579                start: start as usize,
580                end: end as usize,
581                inert,
582            });
583        }
584        /// A function or class expression with no name of its own takes the
585        /// name it is bound to.
586        fn anonymous(expression: &Expression<'_>) -> Option<(oxc_span::Span, &'static str, bool)> {
587            match expression {
588                Expression::FunctionExpression(f) if f.id.is_none() => {
589                    Some((f.span, "function", f.body.is_none()))
590                }
591                Expression::ArrowFunctionExpression(a) => Some((a.span, "function", false)),
592                Expression::ClassExpression(c) if c.id.is_none() => {
593                    Some((c.span, "class", c.declare))
594                }
595                _ => None,
596            }
597        }
598        fn key(key: &PropertyKey<'_>) -> Option<String> {
599            match key {
600                PropertyKey::PrivateIdentifier(id) => Some(format!("#{}", id.name)),
601                _ => key.static_name().map(|n| n.into_owned()),
602            }
603        }
604    }
605    fn with_decorators(span: oxc_span::Span, decorators: &[Decorator<'_>]) -> u32 {
606        decorators
607            .iter()
608            .map(|d| d.span.start)
609            .min()
610            .map_or(span.start, |s| s.min(span.start))
611    }
612    impl<'a> Visit<'a> for Collector<'_> {
613        fn visit_string_literal(&mut self, it: &StringLiteral<'a>) {
614            self.strings
615                .push((it.span.start as usize, it.span.end as usize));
616        }
617        fn visit_template_literal(&mut self, it: &TemplateLiteral<'a>) {
618            self.strings
619                .push((it.span.start as usize, it.span.end as usize));
620            walk::walk_template_literal(self, it);
621        }
622        fn visit_ts_template_literal_type(&mut self, it: &TSTemplateLiteralType<'a>) {
623            self.strings
624                .push((it.span.start as usize, it.span.end as usize));
625            walk::walk_ts_template_literal_type(self, it);
626        }
627        fn visit_function(&mut self, it: &Function<'a>, flags: oxc_syntax::scope::ScopeFlags) {
628            if let Some(id) = &it.id {
629                self.record(
630                    id.name.to_string(),
631                    "function",
632                    it.span.start,
633                    it.span.end,
634                    it.declare || it.body.is_none(),
635                );
636            }
637            walk::walk_function(self, it, flags);
638        }
639        fn visit_class(&mut self, it: &Class<'a>) {
640            if let Some(id) = &it.id {
641                self.record(
642                    id.name.to_string(),
643                    "class",
644                    with_decorators(it.span, &it.decorators),
645                    it.span.end,
646                    it.declare,
647                );
648            }
649            walk::walk_class(self, it);
650        }
651        fn visit_method_definition(&mut self, it: &MethodDefinition<'a>) {
652            if let Some(name) = Self::key(&it.key) {
653                let kind = match it.kind {
654                    MethodDefinitionKind::Constructor => "constructor",
655                    MethodDefinitionKind::Method => "method",
656                    MethodDefinitionKind::Get => "get",
657                    MethodDefinitionKind::Set => "set",
658                };
659                self.record(
660                    name,
661                    kind,
662                    with_decorators(it.span, &it.decorators),
663                    it.span.end,
664                    it.value.body.is_none(),
665                );
666            }
667            walk::walk_method_definition(self, it);
668        }
669        fn visit_property_definition(&mut self, it: &PropertyDefinition<'a>) {
670            if let Some(value) = &it.value
671                && let Some((_, kind, inert)) = Self::anonymous(value)
672                && let Some(name) = Self::key(&it.key)
673            {
674                self.record(
675                    name,
676                    kind,
677                    with_decorators(it.span, &it.decorators),
678                    it.span.end,
679                    inert,
680                );
681            }
682            walk::walk_property_definition(self, it);
683        }
684        fn visit_object_property(&mut self, it: &ObjectProperty<'a>) {
685            if (it.method || Self::anonymous(&it.value).is_some())
686                && let Some(name) = Self::key(&it.key)
687            {
688                let kind = match &it.value {
689                    Expression::ClassExpression(_) => "class",
690                    _ => "method",
691                };
692                self.record(name, kind, it.span.start, it.span.end, false);
693            }
694            walk::walk_object_property(self, it);
695        }
696        fn visit_variable_declarator(&mut self, it: &VariableDeclarator<'a>) {
697            if let Some(init) = &it.init
698                && let Some(id) = it.id.get_binding_identifier()
699            {
700                if let Some((span, kind, inert)) = Self::anonymous(init) {
701                    self.record(id.name.to_string(), kind, span.start, span.end, inert);
702                } else if let Expression::ObjectExpression(object) = init {
703                    self.record(
704                        id.name.to_string(),
705                        "object",
706                        object.span.start,
707                        object.span.end,
708                        false,
709                    );
710                }
711            }
712            walk::walk_variable_declarator(self, it);
713        }
714        fn visit_assignment_expression(&mut self, it: &AssignmentExpression<'a>) {
715            // `exports.handle = () => {}`, `Server.prototype.start = function () {}`,
716            // `module.exports = { ... }`
717            let bound = match &it.right {
718                Expression::ObjectExpression(object) => Some((object.span, "object", false)),
719                other => Self::anonymous(other),
720            };
721            if let Some((span, kind, inert)) = bound {
722                let target =
723                    &self.source[it.left.span().start as usize..it.left.span().end as usize];
724                if !target.is_empty()
725                    && target
726                        .chars()
727                        .all(|c| c.is_alphanumeric() || matches!(c, '_' | '$' | '.' | '#'))
728                {
729                    self.record(target.to_owned(), kind, span.start, span.end, inert);
730                }
731            }
732            walk::walk_assignment_expression(self, it);
733        }
734        fn visit_ts_enum_declaration(&mut self, it: &TSEnumDeclaration<'a>) {
735            self.record(
736                it.id.name.to_string(),
737                "enum",
738                it.span.start,
739                it.span.end,
740                it.declare,
741            );
742            walk::walk_ts_enum_declaration(self, it);
743        }
744        fn visit_ts_module_declaration(&mut self, it: &TSModuleDeclaration<'a>) {
745            let name = match &it.id {
746                TSModuleDeclarationName::Identifier(id) => id.name.to_string(),
747                TSModuleDeclarationName::StringLiteral(s) => s.value.to_string(),
748            };
749            self.record(name, "namespace", it.span.start, it.span.end, it.declare);
750            walk::walk_ts_module_declaration(self, it);
751        }
752        fn visit_ts_global_declaration(&mut self, it: &TSGlobalDeclaration<'a>) {
753            self.record(
754                "global".to_owned(),
755                "namespace",
756                it.span.start,
757                it.span.end,
758                true,
759            );
760            walk::walk_ts_global_declaration(self, it);
761        }
762        fn visit_ts_interface_declaration(&mut self, it: &TSInterfaceDeclaration<'a>) {
763            self.record(
764                it.id.name.to_string(),
765                "interface",
766                it.span.start,
767                it.span.end,
768                true,
769            );
770            walk::walk_ts_interface_declaration(self, it);
771        }
772        fn visit_ts_type_alias_declaration(&mut self, it: &TSTypeAliasDeclaration<'a>) {
773            self.record(
774                it.id.name.to_string(),
775                "type",
776                it.span.start,
777                it.span.end,
778                true,
779            );
780            walk::walk_ts_type_alias_declaration(self, it);
781        }
782    }
783    let mut collector = Collector {
784        source,
785        declarations: vec![],
786        strings: vec![],
787    };
788    collector.visit_program(&parsed.program);
789    Some(Outline {
790        comments,
791        strings: collector.strings,
792        declarations: collector.declarations,
793        separator: ".",
794    })
795}
796
797// -------------------------------------------------------------------- Python
798
799fn python(source: &str) -> Option<Outline> {
800    use ruff_python_ast::{
801        Stmt,
802        visitor::{Visitor, walk_stmt},
803    };
804    use ruff_text_size::Ranged;
805
806    use ruff_python_ast::token::TokenKind;
807    let parsed = ruff_python_parser::parse_module(source).ok()?;
808    let strings = parsed
809        .tokens()
810        .iter()
811        .filter(|t| {
812            matches!(
813                t.kind(),
814                TokenKind::String | TokenKind::FStringMiddle | TokenKind::TStringMiddle
815            ) || (!matches!(
816                t.kind(),
817                TokenKind::Comment | TokenKind::Newline | TokenKind::NonLogicalNewline
818            ) && source[t.range().start().to_usize()..t.range().end().to_usize()]
819                .contains('\n'))
820        })
821        .map(|t| (t.range().start().to_usize(), t.range().end().to_usize()))
822        .collect();
823    let comments = parsed
824        .tokens()
825        .iter()
826        .filter(|t| t.kind() == TokenKind::Comment)
827        .map(|t| (t.range().start().to_usize(), t.range().end().to_usize()))
828        .filter(|(start, end)| {
829            let text = &source[*start..*end];
830            // PEP 263: the encoding declaration is read before anything else.
831            !(universally_significant(text)
832                || text.contains("coding:")
833                || text.contains("coding=")
834                || text.contains("-*-"))
835        })
836        .collect();
837
838    struct Collector(Vec<Item>);
839    impl<'a> Visitor<'a> for Collector {
840        fn visit_stmt(&mut self, stmt: &'a Stmt) {
841            match stmt {
842                Stmt::FunctionDef(def) => {
843                    let start = def
844                        .decorator_list
845                        .iter()
846                        .map(|d| d.range().start())
847                        .min()
848                        .map_or(def.range().start(), |s| s.min(def.range().start()));
849                    self.0.push(Item {
850                        name: def.name.to_string(),
851                        kind: "function",
852                        start: start.to_usize(),
853                        end: def.range().end().to_usize(),
854                        inert: false,
855                    });
856                }
857                Stmt::ClassDef(def) => {
858                    let start = def
859                        .decorator_list
860                        .iter()
861                        .map(|d| d.range().start())
862                        .min()
863                        .map_or(def.range().start(), |s| s.min(def.range().start()));
864                    self.0.push(Item {
865                        name: def.name.to_string(),
866                        kind: "class",
867                        start: start.to_usize(),
868                        end: def.range().end().to_usize(),
869                        inert: false,
870                    });
871                }
872                _ => {}
873            }
874            walk_stmt(self, stmt);
875        }
876    }
877    let mut collector = Collector(vec![]);
878    for stmt in &parsed.syntax().body {
879        collector.visit_stmt(stmt);
880    }
881    Some(Outline {
882        comments,
883        strings,
884        declarations: collector.0,
885        separator: ".",
886    })
887}
888
889// ---------------------------------------------------------------------- Rust
890
891fn rust(source: &str) -> Option<Outline> {
892    use ra_ap_syntax::{
893        AstNode, AstToken, Edition, NodeOrToken, SourceFile, SyntaxKind, ast, ast::HasName,
894    };
895
896    let parsed = SourceFile::parse(source, Edition::Edition2024);
897    if !parsed.errors().is_empty() {
898        return None;
899    }
900    let root = parsed.tree();
901    let mut comments = Vec::new();
902    let mut strings = Vec::new();
903    let mut declarations = Vec::new();
904    // A doctest is a fenced block inside a run of doc comment lines, and each
905    // line is its own token. The run is kept or blanked as one.
906    let mut doc_block: Vec<(usize, usize, bool)> = Vec::new();
907    let flush = |block: &mut Vec<(usize, usize, bool)>, comments: &mut Vec<(usize, usize)>| {
908        if !block.iter().any(|(_, _, fence)| *fence) {
909            comments.extend(block.iter().map(|(s, e, _)| (*s, *e)));
910        }
911        block.clear();
912    };
913    for element in root.syntax().descendants_with_tokens() {
914        match element {
915            NodeOrToken::Token(token) if token.kind() == SyntaxKind::COMMENT => {
916                let text = token.text();
917                let range = token.text_range();
918                let range = (
919                    u32::from(range.start()) as usize,
920                    u32::from(range.end()) as usize,
921                );
922                if universally_significant(text) {
923                    continue;
924                }
925                let doc = ast::Comment::cast(token.clone()).is_some_and(|c| c.kind().doc.is_some());
926                if doc {
927                    doc_block.push((range.0, range.1, text.contains("```")));
928                } else {
929                    flush(&mut doc_block, &mut comments);
930                    comments.push(range);
931                }
932            }
933            NodeOrToken::Token(token) if token.kind() == SyntaxKind::WHITESPACE => {}
934            NodeOrToken::Token(token) => {
935                flush(&mut doc_block, &mut comments);
936                if matches!(
937                    token.kind(),
938                    SyntaxKind::STRING | SyntaxKind::BYTE_STRING | SyntaxKind::C_STRING
939                ) || token.text().contains('\n')
940                {
941                    let range = token.text_range();
942                    strings.push((
943                        u32::from(range.start()) as usize,
944                        u32::from(range.end()) as usize,
945                    ));
946                }
947            }
948            NodeOrToken::Node(node) => {
949                let range = node.text_range();
950                let (start, end) = (
951                    u32::from(range.start()) as usize,
952                    u32::from(range.end()) as usize,
953                );
954                let named = |name: Option<ast::Name>, kind: &'static str| {
955                    name.map(|n| Item {
956                        name: n.text().to_string(),
957                        kind,
958                        start,
959                        end,
960                        inert: false,
961                    })
962                };
963                let declaration = if let Some(item) = ast::Fn::cast(node.clone()) {
964                    named(item.name(), "function")
965                } else if let Some(item) = ast::Impl::cast(node.clone()) {
966                    // `impl<T> Display for Wrapper<T>`: the header is the name.
967                    let header_end = item
968                        .assoc_item_list()
969                        .map_or(end, |l| u32::from(l.syntax().text_range().start()) as usize);
970                    let header = item
971                        .syntax()
972                        .children_with_tokens()
973                        .filter(|e| {
974                            (u32::from(e.text_range().end()) as usize) <= header_end
975                                && !matches!(e.kind(), SyntaxKind::COMMENT | SyntaxKind::ATTR)
976                        })
977                        .map(|e| e.to_string())
978                        .collect::<String>();
979                    Some(Item {
980                        name: collapse_whitespace(&header),
981                        kind: "impl",
982                        start,
983                        end,
984                        inert: false,
985                    })
986                } else if let Some(item) = ast::Struct::cast(node.clone()) {
987                    named(item.name(), "struct")
988                } else if let Some(item) = ast::Enum::cast(node.clone()) {
989                    named(item.name(), "enum")
990                } else if let Some(item) = ast::Union::cast(node.clone()) {
991                    named(item.name(), "union")
992                } else if let Some(item) = ast::Trait::cast(node.clone()) {
993                    named(item.name(), "trait")
994                } else if let Some(item) = ast::TypeAlias::cast(node.clone()) {
995                    named(item.name(), "type")
996                } else if let Some(item) = ast::Const::cast(node.clone()) {
997                    named(item.name(), "const")
998                } else if let Some(item) = ast::Static::cast(node.clone()) {
999                    named(item.name(), "static")
1000                } else if let Some(item) = ast::Module::cast(node.clone()) {
1001                    named(item.name(), "mod")
1002                } else if let Some(item) = ast::MacroRules::cast(node.clone()) {
1003                    named(item.name(), "macro")
1004                } else if let Some(item) = ast::MacroDef::cast(node.clone()) {
1005                    named(item.name(), "macro")
1006                } else {
1007                    None
1008                };
1009                declarations.extend(declaration);
1010            }
1011        }
1012    }
1013    flush(&mut doc_block, &mut comments);
1014    Some(Outline {
1015        comments,
1016        strings,
1017        declarations,
1018        separator: "::",
1019    })
1020}
1021
1022// ---------------------------------------------------------------------- Ruby
1023
1024fn ruby(source: &str) -> Option<Outline> {
1025    use ruby_prism::{
1026        ClassNode, DefNode, InterpolatedRegularExpressionNode, InterpolatedStringNode,
1027        InterpolatedSymbolNode, InterpolatedXStringNode, Location, ModuleNode,
1028        RegularExpressionNode, SingletonClassNode, StringNode, SymbolNode, Visit, XStringNode,
1029    };
1030
1031    let parsed = ruby_prism::parse(source.as_bytes());
1032    if parsed.errors().next().is_some() {
1033        return None;
1034    }
1035    // Magic comments are read by the interpreter before the file runs.
1036    fn magic(text: &str) -> bool {
1037        let body = text.trim_start_matches('#').trim_start();
1038        if body.starts_with("-*-") {
1039            return true;
1040        }
1041        let Some((key, _)) = body.split_once(':') else {
1042            return false;
1043        };
1044        matches!(
1045            key.trim().to_ascii_lowercase().replace('-', "_").as_str(),
1046            "frozen_string_literal"
1047                | "encoding"
1048                | "coding"
1049                | "warn_indent"
1050                | "shareable_constant_value"
1051                | "typed"
1052        )
1053    }
1054    let comments = parsed
1055        .comments()
1056        .map(|c| (c.location().start_offset(), c.location().end_offset()))
1057        .filter(|(start, end)| {
1058            let text = &source[*start..*end];
1059            !(universally_significant(text) || magic(text))
1060        })
1061        .collect();
1062
1063    struct Collector<'s> {
1064        source: &'s str,
1065        declarations: Vec<Item>,
1066        strings: Vec<(usize, usize)>,
1067    }
1068    impl<'s> Collector<'s> {
1069        /// A heredoc's node is its opener; its body sits lines below, so the
1070        /// literal spans from the first of its parts to the last.
1071        fn literal<'pr>(&mut self, parts: impl IntoIterator<Item = Option<Location<'pr>>>) {
1072            let mut span: Option<(usize, usize)> = None;
1073            for part in parts.into_iter().flatten() {
1074                let (s, e) = (part.start_offset(), part.end_offset());
1075                span = Some(span.map_or((s, e), |(a, b)| (a.min(s), b.max(e))));
1076            }
1077            self.strings.extend(span);
1078        }
1079        fn record(&mut self, name: String, kind: &'static str, location: ruby_prism::Location<'_>) {
1080            self.declarations.push(Item {
1081                name,
1082                kind,
1083                start: location.start_offset(),
1084                end: location.end_offset(),
1085                inert: false,
1086            });
1087        }
1088        fn text(&self, location: ruby_prism::Location<'_>) -> String {
1089            collapse_whitespace(&self.source[location.start_offset()..location.end_offset()])
1090        }
1091    }
1092    impl<'pr> Visit<'pr> for Collector<'_> {
1093        fn visit_string_node(&mut self, node: &StringNode<'pr>) {
1094            self.literal([
1095                node.opening_loc(),
1096                Some(node.content_loc()),
1097                node.closing_loc(),
1098            ]);
1099        }
1100        fn visit_interpolated_string_node(&mut self, node: &InterpolatedStringNode<'pr>) {
1101            let parts = node
1102                .parts()
1103                .iter()
1104                .map(|p| Some(p.location()))
1105                .collect::<Vec<_>>();
1106            self.literal(
1107                [node.opening_loc(), node.closing_loc()]
1108                    .into_iter()
1109                    .chain(parts),
1110            );
1111            ruby_prism::visit_interpolated_string_node(self, node);
1112        }
1113        fn visit_x_string_node(&mut self, node: &XStringNode<'pr>) {
1114            self.literal([Some(node.location())]);
1115        }
1116        fn visit_interpolated_x_string_node(&mut self, node: &InterpolatedXStringNode<'pr>) {
1117            let parts = node
1118                .parts()
1119                .iter()
1120                .map(|p| Some(p.location()))
1121                .collect::<Vec<_>>();
1122            self.literal(
1123                [Some(node.opening_loc()), Some(node.closing_loc())]
1124                    .into_iter()
1125                    .chain(parts),
1126            );
1127            ruby_prism::visit_interpolated_x_string_node(self, node);
1128        }
1129        fn visit_symbol_node(&mut self, node: &SymbolNode<'pr>) {
1130            self.literal([Some(node.location())]);
1131        }
1132        fn visit_interpolated_symbol_node(&mut self, node: &InterpolatedSymbolNode<'pr>) {
1133            self.literal([Some(node.location())]);
1134            ruby_prism::visit_interpolated_symbol_node(self, node);
1135        }
1136        fn visit_regular_expression_node(&mut self, node: &RegularExpressionNode<'pr>) {
1137            self.literal([Some(node.location())]);
1138        }
1139        fn visit_interpolated_regular_expression_node(
1140            &mut self,
1141            node: &InterpolatedRegularExpressionNode<'pr>,
1142        ) {
1143            self.literal([Some(node.location())]);
1144            ruby_prism::visit_interpolated_regular_expression_node(self, node);
1145        }
1146        fn visit_def_node(&mut self, node: &DefNode<'pr>) {
1147            let name = String::from_utf8_lossy(node.name().as_slice()).into_owned();
1148            self.record(name, "method", node.location());
1149            ruby_prism::visit_def_node(self, node);
1150        }
1151        fn visit_class_node(&mut self, node: &ClassNode<'pr>) {
1152            let name = self.text(node.constant_path().location());
1153            self.record(name, "class", node.location());
1154            ruby_prism::visit_class_node(self, node);
1155        }
1156        fn visit_module_node(&mut self, node: &ModuleNode<'pr>) {
1157            let name = self.text(node.constant_path().location());
1158            self.record(name, "module", node.location());
1159            ruby_prism::visit_module_node(self, node);
1160        }
1161        fn visit_singleton_class_node(&mut self, node: &SingletonClassNode<'pr>) {
1162            let name = format!("<<{}>", self.text(node.expression().location()));
1163            self.record(name, "singleton", node.location());
1164            ruby_prism::visit_singleton_class_node(self, node);
1165        }
1166    }
1167    let mut collector = Collector {
1168        source,
1169        declarations: vec![],
1170        strings: vec![],
1171    };
1172    collector.visit(&parsed.node());
1173    Some(Outline {
1174        comments,
1175        strings: collector.strings,
1176        declarations: collector.declarations,
1177        separator: ".",
1178    })
1179}
1180
1181// ------------------------------------------------------- Go, Java and Kotlin
1182
1183/// Every node of a tree-sitter tree, in source order.
1184fn tree_nodes(tree: &tree_sitter::Tree) -> Vec<tree_sitter::Node<'_>> {
1185    let mut out = Vec::new();
1186    let mut stack = vec![tree.root_node()];
1187    while let Some(node) = stack.pop() {
1188        out.push(node);
1189        let mut cursor = node.walk();
1190        let children = node.children(&mut cursor).collect::<Vec<_>>();
1191        stack.extend(children.into_iter().rev());
1192    }
1193    out
1194}
1195
1196fn field_text<'t>(node: tree_sitter::Node<'t>, field: &str, source: &str) -> Option<String> {
1197    node.child_by_field_name(field)
1198        .map(|n| collapse_whitespace(&source[n.byte_range()]))
1199}
1200
1201fn first_child_of_kind<'t>(
1202    node: tree_sitter::Node<'t>,
1203    kind: &str,
1204) -> Option<tree_sitter::Node<'t>> {
1205    let mut cursor = node.walk();
1206    node.named_children(&mut cursor).find(|c| c.kind() == kind)
1207}
1208
1209fn go(source: &str) -> Option<Outline> {
1210    let tree = crate::go_instrumenter::parse(source).ok()?;
1211    let mut comments = Vec::new();
1212    let mut strings = Vec::new();
1213    let mut declarations = Vec::new();
1214    for node in tree_nodes(&tree) {
1215        let text = &source[node.byte_range()];
1216        match node.kind() {
1217            "interpreted_string_literal" | "raw_string_literal" | "rune_literal" => {
1218                strings.push((node.start_byte(), node.end_byte()));
1219            }
1220            "comment" => {
1221                // Directives the toolchain reads: `//go:embed`, `//go:build`,
1222                // `//export`, `//line`, and the older `// +build`.
1223                let directive = text.starts_with("//go:")
1224                    || text.starts_with("//export")
1225                    || text.starts_with("//line ")
1226                    || text.starts_with("//sys")
1227                    || text
1228                        .trim_start_matches('/')
1229                        .trim_start()
1230                        .starts_with("+build");
1231                if !directive && !universally_significant(text) {
1232                    comments.push((node.start_byte(), node.end_byte()));
1233                }
1234            }
1235            "function_declaration" => {
1236                if let Some(name) = field_text(node, "name", source) {
1237                    declarations.push(Item {
1238                        name,
1239                        kind: "function",
1240                        start: node.start_byte(),
1241                        end: node.end_byte(),
1242                        inert: false,
1243                    });
1244                }
1245            }
1246            "method_declaration" => {
1247                let receiver = node
1248                    .child_by_field_name("receiver")
1249                    .and_then(|list| first_child_of_kind(list, "parameter_declaration"))
1250                    .and_then(|p| field_text(p, "type", source))
1251                    .map(|t| {
1252                        // `*Server`, `Server[T]` -> `Server`
1253                        t.trim_start_matches(['*', '(', ' '])
1254                            .chars()
1255                            .take_while(|c| c.is_alphanumeric() || *c == '_' || *c == '.')
1256                            .collect::<String>()
1257                    });
1258                if let Some(name) = field_text(node, "name", source) {
1259                    declarations.push(Item {
1260                        name: match receiver {
1261                            Some(r) if !r.is_empty() => format!("{r}.{name}"),
1262                            _ => name,
1263                        },
1264                        kind: "method",
1265                        start: node.start_byte(),
1266                        end: node.end_byte(),
1267                        inert: false,
1268                    });
1269                }
1270            }
1271            "type_spec" | "type_alias" => {
1272                if let Some(name) = field_text(node, "name", source) {
1273                    declarations.push(Item {
1274                        name,
1275                        kind: "type",
1276                        start: node.start_byte(),
1277                        end: node.end_byte(),
1278                        inert: false,
1279                    });
1280                }
1281            }
1282            _ => {}
1283        }
1284    }
1285    Some(Outline {
1286        comments,
1287        strings,
1288        declarations,
1289        separator: ".",
1290    })
1291}
1292
1293/// `name(Type, Type)`: overloads are different declarations and are told
1294/// apart by what they take, not by the order they appear in.
1295fn with_parameters(
1296    name: String,
1297    parameters: Option<tree_sitter::Node<'_>>,
1298    source: &str,
1299) -> String {
1300    let Some(parameters) = parameters else {
1301        return name;
1302    };
1303    let mut cursor = parameters.walk();
1304    let types = parameters
1305        .named_children(&mut cursor)
1306        .filter_map(|p| {
1307            if let Some(kind) = p.child_by_field_name("type") {
1308                return Some(collapse_whitespace(&source[kind.byte_range()]));
1309            }
1310            // Kotlin `name: Type` and Java `Type... name` have no type field;
1311            // the type is the last (Kotlin) or first (Java) named child.
1312            let mut inner = p.walk();
1313            let children = p.named_children(&mut inner).collect::<Vec<_>>();
1314            let kind = if p.kind() == "spread_parameter" {
1315                children.first()
1316            } else {
1317                children.iter().find(|c| {
1318                    !matches!(
1319                        c.kind(),
1320                        "identifier"
1321                            | "simple_identifier"
1322                            | "modifiers"
1323                            | "parameter_modifiers"
1324                            | "annotation"
1325                    )
1326                })
1327            };
1328            kind.map(|k| collapse_whitespace(&source[k.byte_range()]))
1329        })
1330        .collect::<Vec<_>>();
1331    format!("{name}({})", types.join(", "))
1332}
1333
1334fn java(source: &str) -> Option<Outline> {
1335    let tree =
1336        crate::jvm_instrumenter::parse(source, crate::jvm_instrumenter::JvmLanguage::Java).ok()?;
1337    let mut comments = Vec::new();
1338    let mut strings = Vec::new();
1339    let mut declarations = Vec::new();
1340    for node in tree_nodes(&tree) {
1341        let kind = match node.kind() {
1342            "string_literal" | "character_literal" => {
1343                strings.push((node.start_byte(), node.end_byte()));
1344                continue;
1345            }
1346            "line_comment" | "block_comment" => {
1347                if !universally_significant(&source[node.byte_range()]) {
1348                    comments.push((node.start_byte(), node.end_byte()));
1349                }
1350                continue;
1351            }
1352            "class_declaration" => "class",
1353            "interface_declaration" => "interface",
1354            "enum_declaration" => "enum",
1355            "record_declaration" => "record",
1356            "annotation_type_declaration" => "annotation",
1357            "method_declaration" => "method",
1358            "constructor_declaration" => "constructor",
1359            _ => continue,
1360        };
1361        let Some(name) = field_text(node, "name", source) else {
1362            continue;
1363        };
1364        let name = if matches!(kind, "method" | "constructor") {
1365            with_parameters(name, node.child_by_field_name("parameters"), source)
1366        } else {
1367            name
1368        };
1369        declarations.push(Item {
1370            name,
1371            kind,
1372            start: node.start_byte(),
1373            end: node.end_byte(),
1374            inert: false,
1375        });
1376    }
1377    Some(Outline {
1378        comments,
1379        strings,
1380        declarations,
1381        separator: ".",
1382    })
1383}
1384
1385fn kotlin(source: &str) -> Option<Outline> {
1386    let tree = crate::jvm_instrumenter::parse(source, crate::jvm_instrumenter::JvmLanguage::Kotlin)
1387        .ok()?;
1388    let mut comments = Vec::new();
1389    let mut strings = Vec::new();
1390    let mut declarations = Vec::new();
1391    for node in tree_nodes(&tree) {
1392        let (name, kind, inert) = match node.kind() {
1393            "string_literal" | "multiline_string_literal" | "character_literal" => {
1394                strings.push((node.start_byte(), node.end_byte()));
1395                continue;
1396            }
1397            "line_comment" | "block_comment" | "multiline_comment" => {
1398                if !universally_significant(&source[node.byte_range()]) {
1399                    comments.push((node.start_byte(), node.end_byte()));
1400                }
1401                continue;
1402            }
1403            "class_declaration" => (field_text(node, "name", source), "class", false),
1404            "object_declaration" => (field_text(node, "name", source), "object", false),
1405            "companion_object" => (
1406                Some(field_text(node, "name", source).unwrap_or_else(|| "Companion".to_owned())),
1407                "object",
1408                false,
1409            ),
1410            "function_declaration" => (
1411                field_text(node, "name", source).map(|n| {
1412                    with_parameters(
1413                        n,
1414                        first_child_of_kind(node, "function_value_parameters"),
1415                        source,
1416                    )
1417                }),
1418                "function",
1419                false,
1420            ),
1421            "secondary_constructor" => (
1422                Some(with_parameters(
1423                    "constructor".to_owned(),
1424                    first_child_of_kind(node, "function_value_parameters"),
1425                    source,
1426                )),
1427                "constructor",
1428                false,
1429            ),
1430            "property_declaration" => (
1431                first_child_of_kind(node, "variable_declaration")
1432                    .and_then(|v| first_child_of_kind(v, "identifier"))
1433                    .map(|id| collapse_whitespace(&source[id.byte_range()])),
1434                "property",
1435                false,
1436            ),
1437            "anonymous_initializer" => (Some("init".to_owned()), "init", false),
1438            "type_alias" => (
1439                first_child_of_kind(node, "identifier")
1440                    .map(|id| collapse_whitespace(&source[id.byte_range()])),
1441                "type",
1442                true,
1443            ),
1444            _ => continue,
1445        };
1446        let Some(name) = name else {
1447            continue;
1448        };
1449        declarations.push(Item {
1450            name,
1451            kind,
1452            start: node.start_byte(),
1453            end: node.end_byte(),
1454            inert,
1455        });
1456    }
1457    Some(Outline {
1458        comments,
1459        strings,
1460        declarations,
1461        separator: ".",
1462    })
1463}
1464
1465#[cfg(test)]
1466mod tests {
1467    /// `tests affected` compares these digests across runs. If this fails, a
1468    /// change made declaration digests different, so every stored run will
1469    /// read every declaration as changed until it is rerun: make sure that is
1470    /// intended, and only then update the pinned value.
1471    #[test]
1472    fn declaration_digests_are_pinned_to_the_basis_format() {
1473        let samples = [
1474            (
1475                "a.ts",
1476                "// lead\nexport class A {\n  /** doc */\n  run(x: number): string {\n    const s = `a ${x}  b`; // tail\n    return s;\n  }\n}\nexport interface I { a: string }\nexport const f = (y) => y ?? 'z';\n",
1477            ),
1478            (
1479                "a.jsx",
1480                "export function View({ ok }) {\n  return <div title={ok && 'y'}>{ok ? 'yes' : 'no'}</div>;\n}\n",
1481            ),
1482            (
1483                "a.py",
1484                "# lead\nclass A:\n    def run(self, x):\n        \"\"\"doc\"\"\"\n        s = f\"a {x}  b\"  # tail\n        return s\n\ndef f(y):\n    return y or 'z'\n",
1485            ),
1486            (
1487                "a.rs",
1488                "//! crate doc\n/// doc\npub fn run(x: u8) -> u8 {\n    // inner\n    let s = \"a  b\";\n    x + s.len() as u8\n}\nmod m {\n    pub fn g() {}\n}\n",
1489            ),
1490            (
1491                "a.rb",
1492                "# frozen_string_literal: true\nclass A\n  def run(x)\n    s = \"a  b\" # tail\n    x + s.size\n  end\nend\n",
1493            ),
1494            (
1495                "a.go",
1496                "package a\n\n//go:embed x.txt\nvar x string\n\n// Run runs.\nfunc Run(y int) int {\n\treturn y + len(\"a  b\")\n}\n",
1497            ),
1498            (
1499                "A.java",
1500                "package a;\n/** doc */\npublic class A {\n  // tail\n  public int run(int x) { return x + \"a  b\".length(); }\n}\n",
1501            ),
1502            (
1503                "A.kt",
1504                "package a\n// lead\nclass A {\n    fun run(x: Int): Int = x + \"a  b\".length\n}\n",
1505            ),
1506        ];
1507        let mut all = String::new();
1508        for (path, source) in samples {
1509            let code = code(path, source).unwrap_or_else(|| panic!("{path} parses"));
1510            all.push_str(&format!("{path} {} {}", code.semantic, code.structure));
1511            for unit in &code.units {
1512                all.push_str(&format!(" {}={}", unit.path, unit.digest));
1513            }
1514            all.push('\n');
1515        }
1516        let pinned = format!("{:x}", Sha256::digest(all.as_bytes()));
1517        assert_eq!(
1518            pinned, PINNED_DECLARATION_DIGESTS,
1519            "declaration digests changed: every stored run will read as changed; update the pin only if intended\n{all}"
1520        );
1521    }
1522    const PINNED_DECLARATION_DIGESTS: &str =
1523        "e350d052c0bf53390700122e45fb4ad582d97437028c2a207b413cb1f70d5209";
1524
1525    use super::*;
1526
1527    fn paths(code: &Code) -> Vec<(&str, &str)> {
1528        code.units
1529            .iter()
1530            .skip(1)
1531            .map(|u| (u.kind.as_str(), u.path.as_str()))
1532            .collect()
1533    }
1534    fn unit<'c>(code: &'c Code, path: &str) -> &'c Unit {
1535        code.units
1536            .iter()
1537            .find(|u| u.path == path)
1538            .unwrap_or_else(|| panic!("no unit {path} in {:?}", paths(code)))
1539    }
1540
1541    #[test]
1542    fn a_comment_changes_no_digest_in_any_language() {
1543        // Comments are the one thing a program cannot observe, and Supercov's
1544        // pragmas the one exception that every language shares.
1545        let cases: [(&str, &str, &str); 7] = [
1546            (
1547                "a.js",
1548                "// note\nexport function f(x) {\n  return x + 1; // why\n}\n",
1549                "// rewritten\nexport function f(x) {\n  return x + 1; /* changed */\n}\n",
1550            ),
1551            (
1552                "a.py",
1553                "# note\ndef f(x):\n    return x + 1  # why\n",
1554                "# rewritten\ndef f(x):\n    return x + 1  # changed\n",
1555            ),
1556            (
1557                "a.rs",
1558                "// note\npub fn f(x: i32) -> i32 {\n    x + 1 // why\n}\n",
1559                "/// A doc line without a fence.\npub fn f(x: i32) -> i32 {\n    x + 1 /* changed */\n}\n",
1560            ),
1561            (
1562                "a.rb",
1563                "# note\ndef f(x)\n  x + 1 # why\nend\n",
1564                "# rewritten\ndef f(x)\n  x + 1 # changed\nend\n",
1565            ),
1566            (
1567                "a.go",
1568                "package p\n\n// note\nfunc F(x int) int {\n\treturn x + 1 // why\n}\n",
1569                "package p\n\n// rewritten\nfunc F(x int) int {\n\treturn x + 1 /* changed */\n}\n",
1570            ),
1571            (
1572                "A.java",
1573                "// note\nclass A {\n  int f(int x) {\n    return x + 1; // why\n  }\n}\n",
1574                "/** Rewritten. */\nclass A {\n  int f(int x) {\n    return x + 1; /* changed */\n  }\n}\n",
1575            ),
1576            (
1577                "A.kt",
1578                "// note\nfun f(x: Int): Int {\n    return x + 1 // why\n}\n",
1579                "/* rewritten */\nfun f(x: Int): Int {\n    return x + 1 /* changed */\n}\n",
1580            ),
1581        ];
1582        for (path, before, after) in cases {
1583            let a = code(path, before).unwrap_or_else(|| panic!("{path} parses"));
1584            let b = code(path, after).unwrap();
1585            assert_eq!(a.semantic, b.semantic, "{path}: semantic digest");
1586            assert_eq!(a.structure, b.structure, "{path}: structure");
1587            assert_eq!(
1588                a.units
1589                    .iter()
1590                    .map(|u| (&u.path, &u.digest))
1591                    .collect::<Vec<_>>(),
1592                b.units
1593                    .iter()
1594                    .map(|u| (&u.path, &u.digest))
1595                    .collect::<Vec<_>>(),
1596                "{path}: unit digests"
1597            );
1598            assert!(a.units.len() >= 2, "{path}: declares something");
1599        }
1600    }
1601
1602    #[test]
1603    fn a_comment_leaves_no_trace_wherever_it_sat() {
1604        // Adding a comment line, a trailing comment or an inline comment must
1605        // read the same as never having written it.
1606        let plain = code("c.js", "function f(a, b) {\n  return a + b;\n}\n").unwrap();
1607        for commented in [
1608            "// above\nfunction f(a, b) {\n  return a + b;\n}\n",
1609            "function f(a, b) {\n  // inside, on its own line\n  return a + b;\n}\n",
1610            "function f(a, b) { // trailing\n  return a + b;   // and here\n}\n",
1611            "function f(a, /* inline */ b) {\n  return a + /* mid */ b;\n}\n",
1612            "function f(a, b) {\n  return/* glued */ a + b;\n}\n",
1613            "function f(a, b) {\n  /* leading */ return a + b;\n}\n",
1614            "function f(a, b) {\n  return a + b;\n}\n// at the end, no newline",
1615            "function f(a, b) {\n  return a + b;\n}\n\n/* a block\n   over lines */\n",
1616        ] {
1617            let with = code("c.js", commented).unwrap();
1618            assert_eq!(plain.semantic, with.semantic, "{commented:?}");
1619            assert_eq!(
1620                plain.units[1].digest, with.units[1].digest,
1621                "f's own digest: {commented:?}"
1622            );
1623            assert_eq!(plain.units[0].digest, with.units[0].digest, "{commented:?}");
1624        }
1625        let python = code("p.py", "def f():\n    a = 1\n    return a\n").unwrap();
1626        let commented = code(
1627            "p.py",
1628            "def f():\n    a = 1  # set\n    # explain\n    return a\n",
1629        )
1630        .unwrap();
1631        assert_eq!(python.semantic, commented.semantic);
1632        let string = code(
1633            "p.py",
1634            "def f():\n    return '''a\n    # not a comment\n    b'''\n",
1635        )
1636        .unwrap();
1637        let other = code("p.py", "def f():\n    return '''a\n    b'''\n").unwrap();
1638        assert_ne!(string.semantic, other.semantic, "a string is not a comment");
1639    }
1640
1641    #[test]
1642    fn blank_lines_and_trailing_whitespace_are_formatting_outside_strings() {
1643        let cases: [(&str, &str, &str); 7] = [
1644            (
1645                "b.js",
1646                "function f() {\n  return 1;\n}\n",
1647                "\nfunction f() {  \n\n  return 1;\t\n\n}\n\n",
1648            ),
1649            (
1650                "b.py",
1651                "def f():\n    a = 1\n    return a\n",
1652                "def f():\n    a = 1   \n\n    return a\n\n\n",
1653            ),
1654            (
1655                "b.rs",
1656                "fn f() -> i32 {\n    1\n}\n",
1657                "fn f() -> i32 {\n\n    1  \n}\n",
1658            ),
1659            ("b.rb", "def f\n  1\nend\n", "def f\n\n  1  \nend\n\n"),
1660            (
1661                "b.go",
1662                "package p\n\nfunc F() int {\n\treturn 1\n}\n",
1663                "package p\n\n\nfunc F() int {\n\treturn 1  \n\n}\n",
1664            ),
1665            (
1666                "B.java",
1667                "class B {\n  int f() {\n    return 1;\n  }\n}\n",
1668                "class B {\n\n  int f() {  \n    return 1;\n\n  }\n}\n",
1669            ),
1670            (
1671                "B.kt",
1672                "fun f(): Int {\n    return 1\n}\n",
1673                "fun f(): Int {\n\n    return 1   \n}\n\n",
1674            ),
1675        ];
1676        for (path, tidy, loose) in cases {
1677            let a = code(path, tidy).unwrap();
1678            let b = code(path, loose).unwrap();
1679            assert_eq!(a.semantic, b.semantic, "{path}");
1680            assert_eq!(
1681                a.units.iter().map(|u| &u.digest).collect::<Vec<_>>(),
1682                b.units.iter().map(|u| &u.digest).collect::<Vec<_>>(),
1683                "{path}"
1684            );
1685        }
1686        // Inside a literal, the same whitespace is the program's data.
1687        let literal_cases: [(&str, &str, &str); 7] = [
1688            ("s.js", "const t = `a\n\nb`;\n", "const t = `a\nb`;\n"),
1689            ("s.py", "t = '''a  \nb'''\n", "t = '''a\nb'''\n"),
1690            (
1691                "s.rs",
1692                "const T: &str = \"a\n\nb\";\n",
1693                "const T: &str = \"a\nb\";\n",
1694            ),
1695            (
1696                "s.rb",
1697                "T = <<~EOS\n  a\n\n  b\nEOS\n",
1698                "T = <<~EOS\n  a\n  b\nEOS\n",
1699            ),
1700            (
1701                "s.go",
1702                "package p\n\nconst T = `a\n\nb`\n",
1703                "package p\n\nconst T = `a\nb`\n",
1704            ),
1705            (
1706                "S.java",
1707                "class S {\n  String t = \"\"\"\n    a\n\n    b\"\"\";\n}\n",
1708                "class S {\n  String t = \"\"\"\n    a\n    b\"\"\";\n}\n",
1709            ),
1710            (
1711                "S.kt",
1712                "val t = \"\"\"a\n\nb\"\"\"\n",
1713                "val t = \"\"\"a\nb\"\"\"\n",
1714            ),
1715        ];
1716        for (path, with, without) in literal_cases {
1717            let a = code(path, with).unwrap_or_else(|| panic!("{path} parses"));
1718            let b = code(path, without).unwrap();
1719            assert_ne!(
1720                a.semantic, b.semantic,
1721                "{path}: a blank line inside a string is content"
1722            );
1723        }
1724    }
1725
1726    #[test]
1727    fn erased_lines_are_named_so_positions_can_skip_them() {
1728        let x = code(
1729            "e.js",
1730            "// one\n\nfunction f() {\n  // two\n  return 1; // not erased: code here\n\n}\n",
1731        )
1732        .unwrap();
1733        assert_eq!(x.erased, [1, 2, 4, 6]);
1734        assert_eq!(x.code_line(3), 1, "f is the first line of code");
1735        assert_eq!(x.code_line(5), 2);
1736        assert_eq!(x.code_line(7), 3);
1737    }
1738
1739    #[test]
1740    fn a_supercov_pragma_in_a_comment_is_kept() {
1741        let a = code("a.js", "// supercov: observes x\nlet x = 1;\n").unwrap();
1742        let b = code("a.js", "// supercov: observes y\nlet x = 1;\n").unwrap();
1743        assert_ne!(a.semantic, b.semantic);
1744    }
1745
1746    #[test]
1747    fn an_edit_lands_in_its_own_declaration_only() {
1748        let before =
1749            "export function a() {\n  return 1;\n}\nexport function b() {\n  return 2;\n}\n";
1750        let after =
1751            "export function a() {\n  return 1;\n}\nexport function b() {\n  return 2 + 0;\n}\n";
1752        let x = code("m.js", before).unwrap();
1753        let y = code("m.js", after).unwrap();
1754        assert_eq!(unit(&x, "a").digest, unit(&y, "a").digest, "a untouched");
1755        assert_ne!(unit(&x, "b").digest, unit(&y, "b").digest, "b edited");
1756        assert_eq!(x.units[0].digest, y.units[0].digest, "top level untouched");
1757        assert_eq!(x.structure, y.structure);
1758        assert_ne!(x.semantic, y.semantic);
1759    }
1760
1761    #[test]
1762    fn a_class_digest_excludes_its_methods_and_counts_them() {
1763        let before = "class C {\n  a() { return 1; }\n  b() { return 2; }\n}\n";
1764        let x = code("c.js", before).unwrap();
1765        assert_eq!(
1766            paths(&x),
1767            [("class", "C"), ("method", "C.a"), ("method", "C.b")]
1768        );
1769        // Editing a method body leaves the class alone.
1770        let y = code(
1771            "c.js",
1772            "class C {\n  a() { return 1; }\n  b() { return 3; }\n}\n",
1773        )
1774        .unwrap();
1775        assert_eq!(unit(&x, "C").digest, unit(&y, "C").digest);
1776        assert_eq!(x.structure, y.structure);
1777        // Adding a method changes both the class and the structure.
1778        let z = code(
1779            "c.js",
1780            "class C {\n  a() { return 1; }\n  b() { return 2; }\n  c() {}\n}\n",
1781        )
1782        .unwrap();
1783        assert_ne!(unit(&x, "C").digest, unit(&z, "C").digest);
1784        assert_ne!(x.structure, z.structure);
1785        // Renaming a method is a structural change even though the class text
1786        // around the placeholder is unchanged.
1787        let r = code(
1788            "c.js",
1789            "class C {\n  a() { return 1; }\n  d() { return 2; }\n}\n",
1790        )
1791        .unwrap();
1792        assert_eq!(unit(&x, "C").digest, unit(&r, "C").digest);
1793        assert_ne!(x.structure, r.structure);
1794    }
1795
1796    #[test]
1797    fn reordering_two_methods_changes_nothing() {
1798        let x = code(
1799            "c.js",
1800            "class C {\n  a() { return 1; }\n  b() { return 2; }\n}\n",
1801        )
1802        .unwrap();
1803        let y = code(
1804            "c.js",
1805            "class C {\n  b() { return 2; }\n  a() { return 1; }\n}\n",
1806        )
1807        .unwrap();
1808        assert_eq!(unit(&x, "C").digest, unit(&y, "C").digest);
1809        assert_eq!(unit(&x, "C.a").digest, unit(&y, "C.a").digest);
1810        assert_eq!(x.structure, y.structure);
1811    }
1812
1813    #[test]
1814    fn whitespace_is_never_blanked() {
1815        let x = code(
1816            "a.py",
1817            "def f(x):\n    if x:\n        return 1\n    return 2\n",
1818        )
1819        .unwrap();
1820        let y = code(
1821            "a.py",
1822            "def f(x):\n    if x:\n        return 1\n        return 2\n",
1823        )
1824        .unwrap();
1825        assert_ne!(unit(&x, "f").digest, unit(&y, "f").digest);
1826    }
1827
1828    #[test]
1829    fn typescript_types_are_inert() {
1830        let before =
1831            "interface A { x: number }\ntype B = A;\nexport function f(a: A): B { return a; }\n";
1832        let x = code("t.ts", before).unwrap();
1833        assert!(unit(&x, "A").inert && unit(&x, "B").inert && !unit(&x, "f").inert);
1834        // Editing, adding or removing a type changes no digest a program rests on.
1835        let y = code("t.ts", "interface A { x: number; y: string }\ntype B = A;\ntype C = B;\nexport function f(a: A): B { return a; }\n").unwrap();
1836        assert_eq!(x.semantic, y.semantic);
1837        assert_eq!(x.structure, y.structure);
1838        assert_eq!(x.units[0].digest, y.units[0].digest);
1839        assert_eq!(unit(&x, "f").digest, unit(&y, "f").digest);
1840        // An enum is a value: it counts.
1841        let z = code("t.ts", "enum E { A }\n").unwrap();
1842        assert!(!unit(&z, "E").inert);
1843        // An overload signature has no body and is inert; the implementation is not.
1844        let o = code("o.ts", "export function g(a: string): void;\nexport function g(a: number): void;\nexport function g(a: unknown) {}\n").unwrap();
1845        let g = o
1846            .units
1847            .iter()
1848            .filter(|u| u.path.starts_with('g'))
1849            .collect::<Vec<_>>();
1850        assert_eq!(g.iter().filter(|u| u.inert).count(), 2, "{:?}", paths(&o));
1851        assert_eq!(g.iter().filter(|u| !u.inert).count(), 1);
1852    }
1853
1854    #[test]
1855    fn javascript_names_what_a_function_is_bound_to() {
1856        let source = "const handle = async (req) => {};\nexports.run = function () {};\nconst api = {\n  get: () => {},\n  post() {},\n};\nclass S {\n  #secret() {}\n  static create() {}\n  get size() { return 1; }\n  field = () => {};\n}\nit('does a thing', () => {});\n";
1857        let x = code("s.js", source).unwrap();
1858        assert_eq!(
1859            paths(&x),
1860            [
1861                ("function", "handle"),
1862                ("function", "exports.run"),
1863                ("object", "api"),
1864                ("method", "api.get"),
1865                ("method", "api.post"),
1866                ("class", "S"),
1867                ("method", "S.#secret"),
1868                ("method", "S.create"),
1869                ("get", "S.size"),
1870                ("function", "S.field"),
1871            ]
1872        );
1873        // An anonymous callback is part of whatever encloses it.
1874        assert_eq!(x.unit_at(13, 1), 0);
1875    }
1876
1877    #[test]
1878    fn decorators_belong_to_what_they_decorate() {
1879        let py = code("d.py", "@app.route('/x')\ndef handler():\n    return 1\n").unwrap();
1880        assert_eq!(
1881            (unit(&py, "handler").line, unit(&py, "handler").column),
1882            (1, 1)
1883        );
1884        let py2 = code("d.py", "@app.route('/y')\ndef handler():\n    return 1\n").unwrap();
1885        assert_ne!(unit(&py, "handler").digest, unit(&py2, "handler").digest);
1886        assert_eq!(
1887            py.units[0].digest, py2.units[0].digest,
1888            "the route lives in the handler"
1889        );
1890
1891        let ts = code("d.ts", "class C {\n  @Get('/x')\n  handle() {}\n}\n").unwrap();
1892        assert_eq!(unit(&ts, "C.handle").line, 2);
1893        let java = code("D.java", "class D {\n  @Test\n  void t() {}\n}\n").unwrap();
1894        assert_eq!(unit(&java, "D.t()").line, 2);
1895        let rust = code("d.rs", "#[test]\nfn t() {}\n").unwrap();
1896        assert_eq!(unit(&rust, "t").line, 1);
1897    }
1898
1899    #[test]
1900    fn rust_paths_use_the_language_s_separator_and_name_impls_by_header() {
1901        let source = "pub struct W<T>(T);\nimpl<T: std::fmt::Debug> std::fmt::Display for W<T> {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { Ok(()) }\n}\nimpl<T> W<T> {\n    pub fn new(t: T) -> Self { W(t) }\n}\n#[cfg(test)]\nmod tests {\n    #[test]\n    fn it_works() {}\n}\n";
1902        let x = code("w.rs", source).unwrap();
1903        assert_eq!(
1904            paths(&x),
1905            [
1906                ("struct", "W"),
1907                (
1908                    "impl",
1909                    "impl<T: std::fmt::Debug> std::fmt::Display for W<T>"
1910                ),
1911                (
1912                    "function",
1913                    "impl<T: std::fmt::Debug> std::fmt::Display for W<T>::fmt"
1914                ),
1915                ("impl", "impl<T> W<T>"),
1916                ("function", "impl<T> W<T>::new"),
1917                ("mod", "tests"),
1918                ("function", "tests::it_works"),
1919            ]
1920        );
1921    }
1922
1923    #[test]
1924    fn a_rust_doctest_is_code_and_a_plain_doc_comment_is_not() {
1925        let a = code("l.rs", "/// Adds one.\npub fn f(x: i32) -> i32 { x + 1 }\n").unwrap();
1926        let b = code(
1927            "l.rs",
1928            "/// Adds exactly one.\npub fn f(x: i32) -> i32 { x + 1 }\n",
1929        )
1930        .unwrap();
1931        assert_eq!(unit(&a, "f").digest, unit(&b, "f").digest);
1932        let c = code(
1933            "l.rs",
1934            "/// ```\n/// assert_eq!(f(1), 2);\n/// ```\npub fn f(x: i32) -> i32 { x + 1 }\n",
1935        )
1936        .unwrap();
1937        let d = code(
1938            "l.rs",
1939            "/// ```\n/// assert_eq!(f(1), 3);\n/// ```\npub fn f(x: i32) -> i32 { x + 1 }\n",
1940        )
1941        .unwrap();
1942        assert_ne!(unit(&c, "f").digest, unit(&d, "f").digest);
1943    }
1944
1945    #[test]
1946    fn ruby_keeps_magic_comments_and_names_reopened_classes_apart() {
1947        let a = code(
1948            "m.rb",
1949            "# frozen_string_literal: true\nclass A\n  def go; end\nend\n",
1950        )
1951        .unwrap();
1952        let b = code(
1953            "m.rb",
1954            "# frozen_string_literal: false\nclass A\n  def go; end\nend\n",
1955        )
1956        .unwrap();
1957        assert_ne!(a.semantic, b.semantic, "string mutability is behaviour");
1958        let c = code("m.rb", "# a note\nclass A\n  def go; end\nend\n").unwrap();
1959        let d = code("m.rb", "# another note\nclass A\n  def go; end\nend\n").unwrap();
1960        assert_eq!(c.semantic, d.semantic);
1961        let x = code("r.rb", "module M\n  class A\n    def go; end\n    class << self\n      def make; end\n    end\n  end\nend\nclass A\n  def again; end\nend\n").unwrap();
1962        assert_eq!(
1963            paths(&x),
1964            [
1965                ("module", "M"),
1966                ("class", "M.A"),
1967                ("method", "M.A.go"),
1968                ("singleton", "M.A.<<self>"),
1969                ("method", "M.A.<<self>.make"),
1970                ("class", "A"),
1971                ("method", "A.again"),
1972            ]
1973        );
1974    }
1975
1976    #[test]
1977    fn go_keeps_directives_and_names_methods_by_receiver() {
1978        let a = code(
1979            "e.go",
1980            "package p\n\nimport _ \"embed\"\n\n//go:embed a.txt\nvar data string\n",
1981        )
1982        .unwrap();
1983        let b = code(
1984            "e.go",
1985            "package p\n\nimport _ \"embed\"\n\n//go:embed b.txt\nvar data string\n",
1986        )
1987        .unwrap();
1988        assert_ne!(
1989            a.semantic, b.semantic,
1990            "the embed directive chooses the data"
1991        );
1992        let x = code("s.go", "package p\n\ntype Server struct{}\n\nfunc (s *Server) Start() error { return nil }\n\nfunc New() *Server { return &Server{} }\n").unwrap();
1993        assert_eq!(
1994            paths(&x),
1995            [
1996                ("type", "Server"),
1997                ("method", "Server.Start"),
1998                ("function", "New")
1999            ]
2000        );
2001    }
2002
2003    #[test]
2004    fn jvm_overloads_are_named_by_their_parameters() {
2005        let java = code(
2006            "O.java",
2007            "class O {\n  void f(String s) {}\n  void f(int n, String... rest) {}\n  O() {}\n}\n",
2008        )
2009        .unwrap();
2010        assert_eq!(
2011            paths(&java),
2012            [
2013                ("class", "O"),
2014                ("method", "O.f(String)"),
2015                ("method", "O.f(int, String)"),
2016                ("constructor", "O.O()"),
2017            ]
2018        );
2019        let kotlin = code("K.kt", "class K(val x: Int) {\n    init { println(x) }\n    val y: Int get() = x\n    fun f(s: String) {}\n    fun f(n: Int) {}\n    constructor(s: String) : this(s.length)\n    companion object {\n        fun make() = K(1)\n    }\n}\ntypealias Alias = K\n").unwrap();
2020        assert_eq!(
2021            paths(&kotlin),
2022            [
2023                ("class", "K"),
2024                ("init", "K.init"),
2025                ("property", "K.y"),
2026                ("function", "K.f(String)"),
2027                ("function", "K.f(Int)"),
2028                ("constructor", "K.constructor(String)"),
2029                ("object", "K.Companion"),
2030                ("function", "K.Companion.make()"),
2031                ("type", "Alias"),
2032            ]
2033        );
2034        assert!(unit(&kotlin, "Alias").inert);
2035    }
2036
2037    #[test]
2038    fn same_named_declarations_are_numbered_in_order() {
2039        let x = code("p.py", "def f():\n    return 1\ndef f():\n    return 2\n").unwrap();
2040        assert_eq!(paths(&x), [("function", "f"), ("function", "f#2")]);
2041    }
2042
2043    #[test]
2044    fn unit_at_finds_the_innermost_holder() {
2045        let source = "const k = 1;\nclass C {\n  a() {\n    return k;\n  }\n}\nfunction b() {}\n";
2046        let x = code("u.js", source).unwrap();
2047        assert_eq!(x.units[x.unit_at(1, 1)].path, "");
2048        assert_eq!(x.units[x.unit_at(2, 1)].path, "C");
2049        assert_eq!(x.units[x.unit_at(4, 5)].path, "C.a");
2050        assert_eq!(
2051            x.units[x.unit_at(6, 1)].path,
2052            "C",
2053            "the closing brace is the class's"
2054        );
2055        assert_eq!(x.units[x.unit_at(7, 1)].path, "b");
2056        assert_eq!(x.units[x.unit_at(7, 16)].path, "", "past b's end");
2057        assert_eq!(
2058            x.ancestors(x.unit_at(4, 5))
2059                .map(|i| x.units[i].path.clone())
2060                .collect::<Vec<_>>(),
2061            ["C.a", "C", ""]
2062        );
2063    }
2064
2065    #[test]
2066    fn positions_are_one_based_byte_columns_like_anchors() {
2067        let x = code("p.js", "const é = 1; function f() {}\n").unwrap();
2068        let f = unit(&x, "f");
2069        assert_eq!((f.line, f.column), (1, 15), "é is two bytes");
2070        assert_eq!((f.end_line, f.end_column), (1, 30), "end is exclusive");
2071    }
2072
2073    #[test]
2074    fn an_unparsable_or_unknown_file_has_no_view() {
2075        assert!(code("a.js", "function (").is_none());
2076        assert!(code("data.json", "{}").is_none());
2077        assert!(code("README.md", "# hi").is_none());
2078    }
2079
2080    #[test]
2081    fn a_top_level_edit_changes_the_file_unit_and_only_it() {
2082        // Adding a top-level binding can shadow a name every function in the
2083        // file uses; the file unit is what carries that.
2084        let x = code("t.js", "export function f() { return g; }\n").unwrap();
2085        let y = code("t.js", "const g = 1;\nexport function f() { return g; }\n").unwrap();
2086        assert_ne!(x.units[0].digest, y.units[0].digest);
2087        assert_eq!(unit(&x, "f").digest, unit(&y, "f").digest);
2088        assert_eq!(x.structure, y.structure);
2089    }
2090}