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