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