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