1use serde::{Deserialize, Serialize};
23use sha2::{Digest, Sha256};
24use std::collections::{BTreeMap, BTreeSet};
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "camelCase", deny_unknown_fields)]
30pub struct Unit {
31 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 pub digest: String,
43 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub parent: Option<usize>,
45 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
49 pub inert: bool,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(rename_all = "camelCase", deny_unknown_fields)]
55pub struct Code {
56 pub semantic: String,
59 pub structure: String,
63 pub units: Vec<Unit>,
65 #[serde(default, skip_serializing_if = "Vec::is_empty")]
69 pub erased: Vec<usize>,
70}
71
72impl Unit {
73 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 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 pub fn code_line(&self, line: usize) -> usize {
100 line - self.erased.partition_point(|e| *e < line)
101 }
102 pub fn unit_at(&self, line: usize, column: usize) -> usize {
105 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 pub fn ancestors(&self, index: usize) -> impl Iterator<Item = usize> + '_ {
123 std::iter::successors(Some(index), move |i| self.units[*i].parent)
124 }
125 pub fn by_path(&self) -> BTreeMap<&str, &Unit> {
127 self.units.iter().map(|u| (u.path.as_str(), u)).collect()
128 }
129 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#[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 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
188pub 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
198pub 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 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 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 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 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 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 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
420fn 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 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 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 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 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
605struct 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 strings: Vec<(usize, usize)>,
617 declarations: Vec<Item>,
618 separator: &'static str,
619}
620
621pub 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
643fn universally_significant(text: &str) -> bool {
645 text.to_ascii_lowercase().contains("supercov")
646}
647
648fn 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 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 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 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 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
864fn 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
879fn 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 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 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
954fn 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 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 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
1204fn 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 !(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
1296fn 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 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 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
1429fn 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 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 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
1588fn 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 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 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
1700fn 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 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 #[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 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 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 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 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 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 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 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 let z = code("t.ts", "enum E { A }\n").unwrap();
2249 assert!(!unit(&z, "E").inert);
2250 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 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 for changed in [
2494 added.replace("return a(page);", "return a(page, 1);"),
2496 format!("{added}console.log('loaded');\n"),
2498 added.replace(
2500 "import { faker } from '@faker-js/faker';",
2501 "import { faker } from './faker';",
2502 ),
2503 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 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 added.replace("json.dumps(fields)", "json.dumps(fields, sort_keys=True)"),
2538 format!("{added}print('loaded')\n"),
2540 added.replace(
2542 "def make_refund(order):",
2543 "@pytest.fixture\ndef make_refund(order):",
2544 ),
2545 added.replace("import random\n", "from . import randomness as random\n"),
2547 added.replace("return Order(", "return later() or Order("),
2549 format!("{added}__all__ = [\"make_refund\"]\n"),
2551 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 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}