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