1use crate::coverage_analysis::PointKind;
18use crate::coverage_report::CoverageReport;
19use oxc_allocator::Allocator;
20use oxc_ast::ast::{
21 Declaration, ExportDefaultDeclarationKind, Expression, Statement as JsStatement,
22};
23use oxc_ast_visit::{Visit, walk};
24use oxc_parser::Parser;
25use oxc_span::{GetSpan, SourceType};
26use serde::Serialize;
27use serde_json::{Value, json};
28use sha2::{Digest, Sha256};
29use std::collections::{BTreeMap, BTreeSet};
30use std::path::{Path, PathBuf};
31
32mod go;
33mod jvm;
34mod ruby;
35mod rust_source;
36
37type Starts = BTreeMap<usize, Vec<(String, Option<Change>)>>;
40
41pub const MODEL: &str = "jev-1.13.0";
42pub const FIRST_TESTS: usize = 5;
46pub const MORE_TESTS: usize = 15;
47pub const LAST_TESTS: usize = 3;
50pub const MAX_TESTS: usize = MORE_TESTS + LAST_TESTS;
52pub const MAX_QUESTIONS: usize = 150;
53pub const THRESHOLD: f64 = 0.5;
55const HEAD_LINES: usize = 10;
56const HELPER_CHARS: usize = 3000;
57const FIXTURE_CHARS: usize = 12000;
60const CONTEXT_LINES: usize = 0;
61const WINDOW_LINES: usize = 4;
62const OUTSIDE_A_FUNCTION: usize = 25;
65const CODE_CHARS: usize = 10000;
66const BODY_LINES: usize = 150;
67
68pub const INSTRUCTIONS: &str = "Each question describes exactly one change to the program, made alone. The test passes on the code as written and executes the changed line. Question: with that change, does this test fail (an assertion fails, test or helper code throws, the test times out, or the process crashes)? Follow what the changed line computes to what the test checks; do not guess from names. In `code_run`, lines marked ▶ start statements this test ran.";
69
70fn runtime(runner: &str) -> &'static str {
71 match runner {
72 "node:test" => {
73 "node:test; a test past its timeout fails; after hooks still run. The tests spawn the program as child processes; an unhandled promise rejection ends a Node process."
74 }
75 "jest" => {
76 "Jest; a test past its timeout (default 5000 ms) fails; an unhandled error during the run fails it; jest.fn() returns undefined unless given an implementation."
77 }
78 "ava" => {
79 "AVA; a failed t assertion, an uncaught error or a rejected promise the test returns fails it; a test past its timeout (default 10 s) fails; a test with no assertion fails unless failWithoutAssertions is off; a file's tests run concurrently unless declared with test.serial."
80 }
81 "tap" => {
82 "node-tap; a failed t assertion, an uncaught error or a rejected promise a subtest returns fails it; a subtest past its timeout (default 30 s) fails; a parent fails when a subtest does; each test file runs as a process of its own."
83 }
84 "mocha" => {
85 "Mocha; a test past its timeout (default 2000 ms) fails; an uncaught error, a rejected promise it returns or an error passed to done fails it; beforeEach hooks run before each test."
86 }
87 "pytest" => {
88 "pytest; a failing assert or an uncaught exception fails the test; there is no timeout unless pytest-timeout is configured; fixtures from conftest.py run before the test; a Mock or MagicMock returns another mock unless given a return value."
89 }
90 "rspec" => {
91 "RSpec; a failed expectation or an uncaught exception fails the example; there is no timeout; before hooks and let blocks run for each example; a double raises on messages it was not told to expect, and allow(...).to receive returns nil unless given a value."
92 }
93 "minitest" => {
94 "Minitest; a failed assertion or an uncaught exception fails the test; there is no timeout; setup runs before each test; a stubbed method returns the value it was given."
95 }
96 "test-unit" => {
97 "test-unit; a failed assertion or an uncaught exception fails the test; there is no timeout; setup runs before each test."
98 }
99 "cucumber" => {
100 "Cucumber; a step that raises (a failed expectation included) fails the scenario; a step with no definition leaves it undefined, not passed; Before hooks run for each scenario."
101 }
102 "go-test" => {
103 "go test; t.Error, t.Fatal and a panic fail the test; a test past the -timeout (default 10 minutes) fails; subtests run inside their parent; a nil pointer dereference panics. A test catches a changed value only where one of its checks compares it, directly or through what it feeds: a byte, field or result that no check reads can change while the test still passes."
104 }
105 "rust-libtest" | "rust-nextest" | "nextest" => {
106 "Rust tests; a panic fails the test (a failed assert!, assert_eq!, unwrap or expect included) unless it is marked #[should_panic], which then fails when nothing panics; there is no timeout."
107 }
108 "rustdoc" => {
109 "Rust doctests; the code block is compiled and run as its own program, and a panic fails it; a block marked no_run is only compiled and should_panic fails when nothing panics."
110 }
111 "junit-platform" => {
112 "JUnit; a failed assertion or an uncaught exception fails the test (an expected one inside assertThrows aside); @BeforeEach runs before each test; there is no timeout unless one is set; a Mockito mock returns null, 0, false or an empty collection unless stubbed."
113 }
114 "testng" => {
115 "TestNG; a failed assertion or an uncaught exception fails the test; @BeforeMethod runs before each test; there is no timeout unless one is set; a Mockito mock returns null, 0, false or an empty collection unless stubbed."
116 }
117 "unittest" => {
118 "unittest; a failing assert* method or an uncaught exception fails the test; there is no timeout; setUp runs before each test; a Mock or MagicMock returns another mock unless given a return value."
119 }
120 _ => {
121 "Vitest; a test past its timeout (default 5000 ms) fails; an unhandled error during the run fails it; vi.fn() returns undefined unless given an implementation."
122 }
123 }
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
127#[serde(rename_all = "camelCase")]
128pub enum Change {
129 Invert,
131 ReturnUndefined,
133 ValueUndefined,
135 ExpressionUndefined,
137 Skip,
139 Flip,
143 BareError,
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub enum Language {
154 JavaScript,
155 Python,
156 Ruby,
157 Go,
158 Rust,
159 Jvm,
161}
162
163impl Language {
164 fn asks_within_the_function(self) -> bool {
171 matches!(self, Language::JavaScript | Language::Python)
172 }
173
174 pub fn from_manifest(name: &str) -> Option<Self> {
176 match name {
177 "javascript" => Some(Self::JavaScript),
178 "python" => Some(Self::Python),
179 "ruby" => Some(Self::Ruby),
180 "go" => Some(Self::Go),
181 "rust" => Some(Self::Rust),
182 "jvm" => Some(Self::Jvm),
183 _ => None,
184 }
185 }
186 fn comment(self, line: &str) -> bool {
187 let t = line.trim_start();
188 match self {
189 Self::Python | Self::Ruby => t.starts_with('#'),
190 Self::JavaScript | Self::Go | Self::Rust | Self::Jvm => {
191 t.starts_with("//")
192 || t.starts_with("/*")
193 || t.starts_with("*/")
194 || t == "*"
195 || t.starts_with("* ")
196 || t.starts_with("*\t")
197 }
198 }
199 }
200 fn nothing(self) -> &'static str {
202 match self {
203 Self::JavaScript => "undefined",
204 Self::Python => "None",
205 Self::Ruby => "nil",
206 Self::Go => "the zero value",
207 Self::Rust => "`Default::default()`",
208 Self::Jvm => "the default value (null, or 0 or false for a primitive)",
209 }
210 }
211}
212
213fn separator(language: Language, runner: &str) -> &'static str {
215 match (language, runner) {
216 (Language::JavaScript, _) => " > ",
217 (Language::Python, _) => "::",
218 (_, "rspec") => ":",
219 (_, "minitest" | "test-unit" | "junit-platform" | "testng") => "#",
220 (_, "go-test") => "/",
221 (_, "rustdoc") => " - ",
222 _ => "::",
223 }
224}
225
226#[derive(Debug, Clone)]
227pub struct Statement {
228 pub file: String,
229 pub line: usize,
230 pub text: String,
231 pub change: Change,
232 pub replacement: Option<String>,
235 pub tests: Vec<usize>,
237}
238
239#[derive(Debug, Clone)]
240pub struct TestRef {
241 pub id: String,
242 pub file: String,
243 pub name: String,
244 pub runner: String,
245}
246
247pub struct Population {
248 pub root: PathBuf,
249 pub language: Language,
250 pub statements: Vec<Statement>,
251 pub tests: Vec<TestRef>,
252 ran: Vec<BTreeMap<String, BTreeSet<usize>>>,
254 source_files: BTreeSet<String>,
257 jvm_files: Vec<String>,
259 files: std::sync::Mutex<BTreeMap<String, Option<std::sync::Arc<str>>>>,
261 quotes: std::sync::Mutex<std::collections::HashMap<(String, String), bool>>,
263}
264
265#[derive(Debug, Clone, PartialEq, Serialize, serde::Deserialize)]
272pub struct AssessmentInput {
273 pub tests: Vec<InputTest>,
274 pub points: Vec<InputPoint>,
275 pub decisions: Vec<InputPoint>,
276}
277
278#[derive(Debug, Clone, PartialEq, Serialize, serde::Deserialize)]
279pub struct InputTest {
280 pub id: String,
281 pub file: Option<String>,
282 pub name: String,
283 pub role: String,
284 pub runner: String,
285}
286
287#[derive(Debug, Clone, PartialEq, Serialize, serde::Deserialize)]
290pub struct InputPoint {
291 pub file: String,
292 pub line: usize,
293 pub source: String,
294 pub tests: Vec<u32>,
295}
296
297pub fn assessment_input(report: &CoverageReport) -> AssessmentInput {
298 let view = &report.filters.passed;
299 let tests = view
300 .tests
301 .iter()
302 .map(|test| InputTest {
303 id: test.id.clone(),
304 file: test.file.clone(),
305 name: test.name.clone(),
306 role: test.role.clone(),
307 runner: test.provenance.runner.clone(),
308 })
309 .collect::<Vec<_>>();
310 let index = tests
311 .iter()
312 .enumerate()
313 .map(|(i, test)| (test.id.as_str(), i as u32))
314 .collect::<BTreeMap<_, _>>();
315 let indexes = |ids: &[crate::interned::Id]| {
316 ids.iter()
317 .filter_map(|id| index.get(id.as_str()).copied())
318 .collect::<Vec<_>>()
319 };
320 let points = view
321 .points
322 .iter()
323 .filter(|p| p.measured && p.meta.kind == PointKind::Statement)
324 .map(|p| InputPoint {
325 file: p.meta.file.clone(),
326 line: p.meta.line,
327 source: p.meta.source.clone(),
328 tests: indexes(&p.tests),
329 })
330 .collect();
331 let decisions = view
332 .decisions
333 .iter()
334 .filter(|d| d.executed)
335 .map(|d| InputPoint {
336 file: d.meta.file.clone(),
337 line: d.meta.line,
338 source: d.meta.source.clone(),
339 tests: indexes(&d.tests),
340 })
341 .collect();
342 AssessmentInput {
343 tests,
344 points,
345 decisions,
346 }
347}
348
349pub fn population(
350 root: &Path,
351 report: &CoverageReport,
352 sources: &BTreeMap<String, String>,
353 language: Language,
354) -> Result<Population, String> {
355 population_from(root, &assessment_input(report), sources, language)
356}
357
358pub fn population_from(
359 root: &Path,
360 input: &AssessmentInput,
361 sources: &BTreeMap<String, String>,
362 language: Language,
363) -> Result<Population, String> {
364 let mut tests = Vec::new();
365 let jvm_files = if language == Language::Jvm {
367 project_files(root, &["java", "kt"])
368 } else {
369 Vec::new()
370 };
371 let mut index = BTreeMap::<u32, usize>::new();
373 for (position, test) in input.tests.iter().enumerate() {
374 let file = match test.file.as_ref().filter(|f| !f.is_empty()) {
375 Some(file) => file.clone(),
376 None if language == Language::Jvm => {
377 let found = jvm::class_paths(&test.name)
378 .iter()
379 .find_map(|suffix| jvm_files.iter().find(|f| ends_with_path(f, suffix)));
380 match found {
381 Some(file) => file.clone(),
382 None => continue,
383 }
384 }
385 None => continue,
386 };
387 if test.role != "test" {
388 continue;
389 }
390 index.insert(position as u32, tests.len());
391 tests.push(TestRef {
392 id: test.id.clone(),
393 file,
394 name: test.name.clone(),
395 runner: test.runner.clone(),
396 });
397 }
398 let mut changes = BTreeMap::<String, BTreeMap<usize, Vec<(String, Option<Change>)>>>::new();
399 let mut replacements = BTreeMap::<String, rust_source::Replacements>::new();
400 let signatures = if language == Language::Rust {
401 rust_source::signatures(
402 sources
403 .iter()
404 .filter(|(file, _)| file.ends_with(".rs"))
405 .map(|(file, source)| (file.as_str(), source.as_str())),
406 )
407 } else {
408 rust_source::Signatures::default()
409 };
410 let mut statements = Vec::new();
411 let mut seen = BTreeSet::new();
412 let executed = input
416 .points
417 .iter()
418 .map(|p| (&p.file, p.line, &p.source, &p.tests, false));
419 let elifs = input
420 .decisions
421 .iter()
422 .filter(|_| language != Language::JavaScript)
423 .map(|d| (&d.file, d.line, &d.source, &d.tests, true));
424 for (file, line, text, point_tests, decision) in executed.chain(elifs) {
425 let key = (file.clone(), line);
426 if !seen.insert(key) {
427 continue; }
429 let ids = point_tests
430 .iter()
431 .filter_map(|position| index.get(position).copied())
432 .collect::<Vec<_>>();
433 if ids.is_empty() {
434 continue;
435 }
436 if language == Language::Rust && rust_source::test_path(file) {
437 continue;
438 }
439 let Some(source) = sources.get(file) else {
440 continue;
441 };
442 let lines = changes
443 .entry(file.clone())
444 .or_insert_with(|| match language {
445 Language::JavaScript => statement_starts(file, source),
446 Language::Python => python_statement_starts(source),
447 Language::Ruby => ruby::statement_starts(source),
448 Language::Go => go::statement_starts(source),
449 Language::Rust => rust_source::statement_starts(source),
450 Language::Jvm => jvm::statement_starts(file, source),
451 });
452 let Some(change) = change_at(lines, line, text, language) else {
453 continue;
454 };
455 if decision && change != Change::Invert {
456 continue;
457 }
458 let valued = matches!(change, Change::ReturnUndefined | Change::ValueUndefined);
459 let replacement = match language {
460 Language::Rust if valued => {
461 let first = prefix(text.lines().next().unwrap_or("").trim(), 30);
462 let first = first.trim_end_matches(';');
463 replacements
464 .entry(file.clone())
465 .or_insert_with(|| rust_source::replacements(file, source, &signatures))
466 .get(&line)
467 .and_then(|all| all.iter().find(|(t, _)| t.starts_with(first)))
468 .map(|(_, r)| r.clone())
469 }
470 Language::Go if valued => go::rewrite(source, line, change),
475 _ => None,
476 };
477 statements.push(Statement {
478 file: file.clone(),
479 line,
480 text: text.clone(),
481 change,
482 replacement,
483 tests: ids,
484 });
485 }
486 let mut ran = vec![BTreeMap::<String, BTreeSet<usize>>::new(); tests.len()];
487 for s in &statements {
488 for &t in &s.tests {
489 ran[t].entry(s.file.clone()).or_default().insert(s.line);
490 }
491 }
492 let source_files = statements.iter().map(|s| s.file.clone()).collect();
493 Ok(Population {
494 root: root.to_path_buf(),
495 language,
496 statements,
497 tests,
498 ran,
499 source_files,
500 jvm_files,
501 files: Default::default(),
502 quotes: Default::default(),
503 })
504}
505
506fn name_words(text: &str) -> BTreeSet<String> {
509 const FILLER: [&str; 13] = [
510 "test", "tests", "the", "and", "for", "with", "get", "set", "self", "from", "not", "does",
511 "should",
512 ];
513 let mut words = BTreeSet::new();
514 let chars = text.chars().collect::<Vec<_>>();
515 let mut word = String::new();
516 let mut flush = |word: &mut String| {
517 let w = word.to_lowercase();
518 if w.len() > 2 && !FILLER.contains(&w.as_str()) {
519 words.insert(w);
520 }
521 word.clear();
522 };
523 for (i, &c) in chars.iter().enumerate() {
524 if !c.is_ascii_alphanumeric() {
525 flush(&mut word);
526 continue;
527 }
528 let previous = i.checked_sub(1).map(|p| chars[p]);
529 let next = chars.get(i + 1).copied();
530 let starts = c.is_ascii_uppercase()
533 && (previous.is_some_and(|p| p.is_ascii_lowercase() || p.is_ascii_digit())
534 || (previous.is_some_and(|p| p.is_ascii_uppercase())
535 && next.is_some_and(|n| n.is_ascii_lowercase())));
536 if starts {
537 flush(&mut word);
538 }
539 word.push(c);
540 }
541 flush(&mut word);
542 words
543}
544
545fn statement_starts(file: &str, source: &str) -> BTreeMap<usize, Vec<(String, Option<Change>)>> {
548 let allocator = Allocator::default();
549 let source_type = SourceType::from_path(file).unwrap_or_else(|_| SourceType::tsx());
550 let parsed = Parser::new(&allocator, source, source_type).parse();
551 let starts = line_starts(source);
552 struct Collector<'s> {
553 source: &'s str,
554 starts: &'s [usize],
555 typescript: bool,
556 out: BTreeMap<usize, Vec<(String, Option<Change>)>>,
557 }
558 impl<'a> Visit<'a> for Collector<'_> {
559 fn visit_statement(&mut self, statement: &JsStatement<'a>) {
560 let span = statement.span();
561 let line = line_of(self.starts, span.start as usize);
562 let text = self
563 .source
564 .get(span.start as usize..span.end as usize)
565 .unwrap_or("")
566 .to_owned();
567 self.out.entry(line).or_default().push((
568 text,
569 match statement {
570 JsStatement::ThrowStatement(_) if self.typescript => Some(Change::BareError),
571 _ => change_of(statement),
572 },
573 ));
574 walk::walk_statement(self, statement);
575 }
576 }
577 let mut collector = Collector {
578 source,
579 starts: &starts,
580 typescript: source_type.is_typescript(),
581 out: BTreeMap::new(),
582 };
583 collector.visit_program(&parsed.program);
584 collector.out
585}
586
587fn is_undefined(expression: &Expression) -> bool {
591 match expression {
592 Expression::Identifier(identifier) => identifier.name == "undefined",
593 Expression::UnaryExpression(unary) => {
594 unary.operator == oxc_syntax::operator::UnaryOperator::Void
595 }
596 Expression::TSAsExpression(cast) => is_undefined(&cast.expression),
597 Expression::TSSatisfiesExpression(cast) => is_undefined(&cast.expression),
598 Expression::TSTypeAssertion(cast) => is_undefined(&cast.expression),
599 Expression::TSNonNullExpression(cast) => is_undefined(&cast.expression),
600 Expression::ParenthesizedExpression(inner) => is_undefined(&inner.expression),
601 _ => false,
602 }
603}
604
605fn change_of(statement: &JsStatement) -> Option<Change> {
606 Some(match statement {
607 JsStatement::IfStatement(_) => Change::Invert,
608 JsStatement::ReturnStatement(r) => match &r.argument {
611 Some(argument) if is_undefined(argument) => Change::Skip,
612 Some(Expression::BooleanLiteral(_)) => Change::Flip,
613 Some(_) => Change::ReturnUndefined,
614 None => Change::Skip,
615 },
616 JsStatement::VariableDeclaration(d) => {
617 if let [only] = d.declarations.as_slice()
618 && matches!(only.init, Some(Expression::BooleanLiteral(_)))
619 {
620 Change::Flip
621 } else if d.declarations.iter().any(|d| d.init.is_some()) {
622 Change::ValueUndefined
623 } else {
624 return None;
625 }
626 }
627 JsStatement::ExpressionStatement(e)
630 if matches!(&e.expression, Expression::CallExpression(call)
631 if matches!(call.callee, Expression::Super(_))) =>
632 {
633 return None;
634 }
635 JsStatement::ExportNamedDeclaration(e) => match &e.declaration {
636 Some(Declaration::VariableDeclaration(d))
637 if d.declarations.iter().any(|d| d.init.is_some()) =>
638 {
639 Change::ValueUndefined
640 }
641 _ => return None,
642 },
643 JsStatement::ExportDefaultDeclaration(e) => match &e.declaration {
644 ExportDefaultDeclarationKind::FunctionDeclaration(_)
645 | ExportDefaultDeclarationKind::ClassDeclaration(_)
646 | ExportDefaultDeclarationKind::TSInterfaceDeclaration(_) => return None,
647 _ => Change::Skip,
648 },
649 JsStatement::ImportDeclaration(_)
650 | JsStatement::ExportAllDeclaration(_)
651 | JsStatement::FunctionDeclaration(_)
652 | JsStatement::ClassDeclaration(_)
653 | JsStatement::TSTypeAliasDeclaration(_)
654 | JsStatement::TSInterfaceDeclaration(_)
655 | JsStatement::TSEnumDeclaration(_)
656 | JsStatement::TSModuleDeclaration(_)
657 | JsStatement::TSGlobalDeclaration(_)
658 | JsStatement::TSImportEqualsDeclaration(_)
659 | JsStatement::TSExportAssignment(_)
660 | JsStatement::TSNamespaceExportDeclaration(_) => return None,
661 _ => Change::Skip,
662 })
663}
664
665fn change_at(
669 starts: &BTreeMap<usize, Vec<(String, Option<Change>)>>,
670 line: usize,
671 text: &str,
672 language: Language,
673) -> Option<Change> {
674 let first = prefix(text.lines().next().unwrap_or("").trim(), 30);
675 let found = starts.get(&line).and_then(|all| {
676 all.iter().find(|(t, _)| {
677 let bare = t
678 .strip_prefix("export ")
679 .map(|t| t.strip_prefix("default ").unwrap_or(t))
680 .unwrap_or(t);
681 bare.starts_with(first) || t.starts_with(first)
682 })
683 });
684 match (found, language) {
685 (Some((_, change)), _) => *change,
686 (None, Language::JavaScript) => Some(Change::ExpressionUndefined),
687 (None, _) => None,
690 }
691}
692
693fn python_statement_starts(source: &str) -> BTreeMap<usize, Vec<(String, Option<Change>)>> {
698 use ruff_python_ast::{
699 Expr, Stmt,
700 visitor::{Visitor, walk_stmt},
701 };
702 use ruff_text_size::Ranged;
703 struct Collector<'s> {
704 source: &'s str,
705 starts: Vec<usize>,
706 out: BTreeMap<usize, Vec<(String, Option<Change>)>>,
707 }
708 impl<'a> Visitor<'a> for Collector<'_> {
709 fn visit_stmt(&mut self, stmt: &'a Stmt) {
710 let change = match stmt {
711 Stmt::If(_) => Some(Change::Invert),
712 Stmt::Return(r) => Some(match r.value.as_deref() {
713 Some(Expr::NoneLiteral(_)) | None => Change::Skip,
715 Some(Expr::BooleanLiteral(_)) => Change::Flip,
716 Some(_) => Change::ReturnUndefined,
717 }),
718 Stmt::Assign(a)
719 if a.targets.len() == 1
720 && matches!(a.value.as_ref(), Expr::BooleanLiteral(_)) =>
721 {
722 Some(Change::Flip)
723 }
724 Stmt::Assign(_) => Some(Change::ValueUndefined),
725 Stmt::AnnAssign(a)
726 if matches!(a.value.as_deref(), Some(Expr::BooleanLiteral(_))) =>
727 {
728 Some(Change::Flip)
729 }
730 Stmt::AnnAssign(a) => a.value.as_ref().map(|_| Change::ValueUndefined),
731 Stmt::Import(_)
732 | Stmt::ImportFrom(_)
733 | Stmt::FunctionDef(_)
734 | Stmt::ClassDef(_)
735 | Stmt::Pass(_)
736 | Stmt::Global(_)
737 | Stmt::Nonlocal(_)
738 | Stmt::TypeAlias(_)
739 | Stmt::IpyEscapeCommand(_) => None,
740 Stmt::Expr(e) if matches!(e.value.as_ref(), Expr::StringLiteral(_)) => None,
741 _ => Some(Change::Skip),
742 };
743 let range = stmt.range();
744 let (start, end) = (range.start().to_usize(), range.end().to_usize());
745 let line = line_of(&self.starts, start);
746 let text = self.source.get(start..end).unwrap_or("").to_owned();
747 self.out.entry(line).or_default().push((text, change));
748 if let Stmt::If(s) = stmt {
750 for test in s.elif_else_clauses.iter().filter_map(|c| c.test.as_ref()) {
752 let start = test.range().start().to_usize();
753 let text = self.source.get(start..test.range().end().to_usize());
754 self.out
755 .entry(line_of(&self.starts, start))
756 .or_default()
757 .push((text.unwrap_or("").to_owned(), Some(Change::Invert)));
758 }
759 }
760 walk_stmt(self, stmt);
761 }
762 }
763 let Ok(parsed) = ruff_python_parser::parse_module(source) else {
764 return BTreeMap::new();
765 };
766 let mut collector = Collector {
767 source,
768 starts: line_starts(source),
769 out: BTreeMap::new(),
770 };
771 for stmt in &parsed.syntax().body {
772 collector.visit_stmt(stmt);
773 }
774 collector.out
775}
776
777fn prefix(text: &str, chars: usize) -> &str {
778 match text.char_indices().nth(chars) {
779 Some((i, _)) => &text[..i],
780 None => text,
781 }
782}
783
784fn outermost_functions(language: Language, file: &str, source: &str) -> Vec<(usize, usize)> {
789 let starts = line_starts(source);
790 let lines = |from: usize, to: usize| {
791 (
792 line_of(&starts, from),
793 line_of(&starts, to.saturating_sub(1).max(from)),
794 )
795 };
796 let mut spans = match language {
797 Language::JavaScript => {
798 let allocator = Allocator::default();
799 let source_type = SourceType::from_path(file).unwrap_or_else(|_| SourceType::tsx());
800 let parsed = Parser::new(&allocator, source, source_type).parse();
801 #[derive(Default)]
802 struct Functions {
803 within: Option<(u32, u32)>,
807 found: Vec<(u32, u32)>,
808 }
809 impl Functions {
810 fn inside(&mut self, span: (u32, u32), walk: impl FnOnce(&mut Self)) {
811 let outer = self.within.replace(span);
812 walk(self);
813 self.within = outer;
814 }
815 fn function(&mut self, own: oxc_span::Span) {
816 self.found.push(self.within.unwrap_or((own.start, own.end)));
817 }
818 }
819 fn from_decorators(
820 span: oxc_span::Span,
821 decorators: &[oxc_ast::ast::Decorator<'_>],
822 ) -> (u32, u32) {
823 (
824 decorators
825 .first()
826 .map_or(span.start, |first| first.span.start.min(span.start)),
827 span.end,
828 )
829 }
830 impl<'a> Visit<'a> for Functions {
831 fn visit_program(&mut self, program: &oxc_ast::ast::Program<'a>) {
832 for statement in &program.body {
833 let span = statement.span();
834 self.inside((span.start, span.end), |this| {
835 this.visit_statement(statement)
836 });
837 }
838 }
839 fn visit_block_statement(&mut self, block: &oxc_ast::ast::BlockStatement<'a>) {
840 for statement in &block.body {
841 let span = statement.span();
842 self.inside((span.start, span.end), |this| {
843 this.visit_statement(statement)
844 });
845 }
846 }
847 fn visit_ts_module_block(&mut self, block: &oxc_ast::ast::TSModuleBlock<'a>) {
848 for statement in &block.body {
849 let span = statement.span();
850 self.inside((span.start, span.end), |this| {
851 this.visit_statement(statement)
852 });
853 }
854 }
855 fn visit_method_definition(&mut self, it: &oxc_ast::ast::MethodDefinition<'a>) {
856 self.inside(from_decorators(it.span, &it.decorators), |this| {
857 walk::walk_method_definition(this, it)
858 });
859 }
860 fn visit_property_definition(&mut self, it: &oxc_ast::ast::PropertyDefinition<'a>) {
861 self.inside(from_decorators(it.span, &it.decorators), |this| {
862 walk::walk_property_definition(this, it)
863 });
864 }
865 fn visit_accessor_property(&mut self, it: &oxc_ast::ast::AccessorProperty<'a>) {
866 self.inside(from_decorators(it.span, &it.decorators), |this| {
867 walk::walk_accessor_property(this, it)
868 });
869 }
870 fn visit_static_block(&mut self, it: &oxc_ast::ast::StaticBlock<'a>) {
871 self.inside((it.span.start, it.span.end), |this| {
872 walk::walk_static_block(this, it)
873 });
874 }
875 fn visit_function(
877 &mut self,
878 it: &oxc_ast::ast::Function<'a>,
879 _flags: oxc_syntax::scope::ScopeFlags,
880 ) {
881 if it.body.is_some() {
882 self.function(it.span);
883 }
884 }
885 fn visit_arrow_function_expression(
886 &mut self,
887 it: &oxc_ast::ast::ArrowFunctionExpression<'a>,
888 ) {
889 self.function(it.span);
890 }
891 }
892 let mut functions = Functions::default();
893 functions.visit_program(&parsed.program);
894 functions
895 .found
896 .into_iter()
897 .map(|(from, to)| lines(from as usize, to as usize))
898 .collect::<Vec<_>>()
899 }
900 Language::Python => {
901 use ruff_python_ast::{
902 Stmt,
903 visitor::{Visitor, walk_stmt},
904 };
905 use ruff_text_size::Ranged;
906 struct Functions(Vec<(usize, usize)>);
907 impl<'a> Visitor<'a> for Functions {
908 fn visit_stmt(&mut self, stmt: &'a Stmt) {
909 match stmt {
910 Stmt::FunctionDef(function) => {
911 let from = function
912 .decorator_list
913 .first()
914 .map_or(function.start(), |first| {
915 first.start().min(function.start())
916 });
917 self.0.push((from.to_usize(), function.end().to_usize()));
918 }
919 _ => walk_stmt(self, stmt),
920 }
921 }
922 }
923 let Ok(parsed) = ruff_python_parser::parse_module(source) else {
924 return Vec::new();
925 };
926 let mut functions = Functions(Vec::new());
927 for stmt in &parsed.syntax().body {
928 functions.visit_stmt(stmt);
929 }
930 functions
931 .0
932 .into_iter()
933 .map(|(from, to)| lines(from, to))
934 .collect()
935 }
936 _ => Vec::new(),
937 };
938 spans.sort_unstable();
939 spans.dedup();
940 spans
941}
942
943fn whole_statement(statement: &Statement) -> Option<usize> {
946 let lines = statement.text.lines().count();
947 let word = statement
948 .text
949 .lines()
950 .next()
951 .unwrap_or("")
952 .trim()
953 .trim_end_matches(';')
954 .trim();
955 (statement.change == Change::Skip
956 && lines > 1
957 && !matches!(word, "break" | "continue" | "next"))
958 .then_some(lines)
959}
960
961fn line_starts(source: &str) -> Vec<usize> {
962 std::iter::once(0)
963 .chain(source.match_indices('\n').map(|(i, _)| i + 1))
964 .collect()
965}
966
967fn line_of(starts: &[usize], offset: usize) -> usize {
968 starts.partition_point(|&s| s <= offset)
969}
970
971fn numbered(lines: &[&str]) -> String {
972 lines
973 .iter()
974 .enumerate()
975 .map(|(i, l)| format!("{:5} {l}", i + 1))
976 .collect::<Vec<_>>()
977 .join("\n")
978}
979
980fn quoted_fragments(text: &str) -> Vec<String> {
985 let mut out = Vec::new();
986 let mut chars = text.char_indices().peekable();
987 while let Some((start, quote)) = chars.next() {
988 if !matches!(quote, '\'' | '"' | '`') {
989 continue;
990 }
991 let mut end = None;
992 while let Some((at, c)) = chars.next() {
993 if c == '\\' {
994 chars.next();
995 } else if c == quote {
996 end = Some(at);
997 break;
998 }
999 }
1000 let Some(end) = end else { break };
1001 let body = &text[start + quote.len_utf8()..end];
1002 let mut pieces = vec![String::new()];
1005 let mut depth = 0usize;
1006 for c in body.chars() {
1007 match c {
1008 '{' => {
1009 depth += 1;
1010 pieces.push(String::new());
1011 }
1012 '}' if depth > 0 => depth -= 1,
1013 '$' | '#' | '%' if depth == 0 => pieces.push(String::new()),
1014 _ if depth == 0 => pieces.last_mut().expect("one piece").push(c),
1015 _ => {}
1016 }
1017 }
1018 for piece in pieces {
1019 let piece = piece.trim();
1020 if piece.len() >= 6 && piece.chars().filter(|c| c.is_alphabetic()).count() >= 3 {
1021 out.push(piece.to_owned());
1022 }
1023 }
1024 }
1025 out.sort();
1026 out.dedup();
1027 out
1028}
1029
1030fn stem(path: &str) -> String {
1031 Path::new(path)
1032 .file_name()
1033 .and_then(|n| n.to_str())
1034 .unwrap_or("")
1035 .split('.')
1036 .next()
1037 .unwrap_or("")
1038 .to_lowercase()
1039}
1040
1041fn describe(name: &str, separator: &str) -> String {
1042 let parts = name.split(separator).collect::<Vec<_>>();
1043 parts[..parts.len().saturating_sub(1)].join(separator)
1044}
1045
1046impl Population {
1047 pub fn with_files(self, files: BTreeMap<String, String>) -> Self {
1050 {
1051 let mut cache = self
1052 .files
1053 .lock()
1054 .expect("the file cache is only read and filled");
1055 for (file, text) in files {
1056 cache.insert(file, Some(text.into()));
1057 }
1058 }
1059 self
1060 }
1061
1062 pub fn files_read(&self) -> BTreeMap<String, String> {
1064 self.files
1065 .lock()
1066 .expect("the file cache is only read and filled")
1067 .iter()
1068 .filter_map(|(file, text)| text.as_ref().map(|t| (file.clone(), t.to_string())))
1069 .collect()
1070 }
1071
1072 fn read(&self, file: &str) -> Option<std::sync::Arc<str>> {
1073 let mut files = self
1074 .files
1075 .lock()
1076 .expect("the file cache is only read and filled");
1077 files
1078 .entry(file.to_owned())
1079 .or_insert_with(|| {
1080 std::fs::read_to_string(self.root.join(file))
1081 .ok()
1082 .map(Into::into)
1083 })
1084 .clone()
1085 }
1086
1087 fn quotes(&self, test: usize, fragments: &[String]) -> bool {
1089 let file = &self.tests[test].file;
1090 fragments.iter().any(|fragment| {
1091 let key = (file.clone(), fragment.clone());
1092 if let Some(&known) = self
1093 .quotes
1094 .lock()
1095 .expect("the quote cache is only read and filled")
1096 .get(&key)
1097 {
1098 return known;
1099 }
1100 let found = self
1101 .read(file)
1102 .is_some_and(|text| text.contains(fragment.as_str()));
1103 self.quotes
1104 .lock()
1105 .expect("the quote cache is only read and filled")
1106 .insert(key, found);
1107 found
1108 })
1109 }
1110
1111 pub fn order(&self, statement: usize, k: usize) -> Vec<usize> {
1118 let s = &self.statements[statement];
1119 let base = stem(&s.file);
1120 let key = |&t: &usize| {
1121 let test = &self.tests[t];
1122 (
1123 !Path::new(&test.file)
1124 .file_name()
1125 .and_then(|n| n.to_str())
1126 .unwrap_or("")
1127 .to_lowercase()
1128 .contains(&base),
1129 test.file.clone(),
1130 test.name.clone(),
1131 )
1132 };
1133 let mut all = s.tests.clone();
1134 all.sort_by_key(key);
1135 if all.len() <= k {
1136 return all;
1137 }
1138 let mut groups = Vec::<((String, String), Vec<usize>)>::new();
1139 for &t in &all {
1140 let g = (
1141 self.tests[t].file.clone(),
1142 describe(
1143 &self.tests[t].name,
1144 separator(self.language, &self.tests[t].runner),
1145 ),
1146 );
1147 match groups.iter_mut().find(|(k, _)| *k == g) {
1148 Some((_, v)) => v.push(t),
1149 None => groups.push((g, vec![t])),
1150 }
1151 }
1152 let mut by_file = Vec::<(String, Vec<Vec<usize>>)>::new();
1153 for ((file, _), members) in groups {
1154 match by_file.iter_mut().find(|(f, _)| *f == file) {
1155 Some((_, v)) => v.push(members),
1156 None => by_file.push((file, vec![members])),
1157 }
1158 }
1159 by_file.sort_by_key(|(f, _)| {
1160 (
1161 !Path::new(f)
1162 .file_name()
1163 .and_then(|n| n.to_str())
1164 .unwrap_or("")
1165 .to_lowercase()
1166 .contains(&base),
1167 f.clone(),
1168 )
1169 });
1170 let mut picked = Vec::new();
1171 let fragments = quoted_fragments(&s.text);
1172 if !fragments.is_empty() {
1173 let quoting = all
1174 .iter()
1175 .copied()
1176 .filter(|&t| self.quotes(t, &fragments))
1177 .collect::<Vec<_>>();
1178 let mut files = BTreeSet::new();
1179 let (first, rest): (Vec<usize>, Vec<usize>) = quoting
1180 .iter()
1181 .partition(|&&t| files.insert(self.tests[t].file.clone()));
1182 for t in first.into_iter().chain(rest) {
1183 if picked.len() < k {
1184 picked.push(t);
1185 }
1186 }
1187 }
1188 let mut depth = 0;
1189 while picked.len() < k && by_file.iter().any(|(_, g)| g.len() > depth) {
1190 for (_, g) in &by_file {
1191 if depth < g.len() && picked.len() < k && !picked.contains(&g[depth][0]) {
1192 picked.push(g[depth][0]);
1193 }
1194 }
1195 depth += 1;
1196 }
1197 for t in all {
1198 if picked.len() >= k {
1199 break;
1200 }
1201 if !picked.contains(&t) {
1202 picked.push(t);
1203 }
1204 }
1205 picked
1206 }
1207
1208 pub fn last_order(&self, statement: usize, asked: &[usize], k: usize) -> Vec<usize> {
1219 let spread = self.order(statement, MORE_TESTS);
1220 let position = |t: &usize| spread.iter().position(|x| x == t).unwrap_or(usize::MAX);
1221 let s = &self.statements[statement];
1222 let line = name_words(s.text.lines().next().unwrap_or(""));
1223 let shared = |t: usize| {
1224 let name = &self.tests[t].name;
1225 let name = name.rsplit("::").next().unwrap_or(name);
1226 name_words(name).intersection(&line).count()
1227 };
1228 let mut rest = s
1229 .tests
1230 .iter()
1231 .copied()
1232 .filter(|t| !asked.contains(t))
1233 .collect::<Vec<_>>();
1234 rest.sort_by_cached_key(|t| {
1235 (
1236 std::cmp::Reverse(shared(*t)),
1237 self.breadth(*t),
1238 position(t),
1239 *t,
1240 )
1241 });
1242 rest.truncate(k);
1243 rest
1244 }
1245
1246 fn breadth(&self, test: usize) -> usize {
1248 self.ran[test].values().map(BTreeSet::len).sum()
1249 }
1250
1251 pub fn test_code(&self, test: usize) -> String {
1254 match self.language {
1255 Language::Python => return self.python_test_code(test),
1256 Language::JavaScript => {}
1257 _ => return self.located_test_code(test),
1258 }
1259 let t = &self.tests[test];
1260 let text = self.read(&t.file).unwrap_or_default();
1261 let lines = text.split('\n').collect::<Vec<_>>();
1262 let title = t.name.rsplit(" > ").next().unwrap_or(&t.name);
1263 let needle = prefix(title, 40);
1264 let start = (!needle.is_empty())
1265 .then(|| lines.iter().position(|l| l.contains(needle)))
1266 .flatten();
1267 let fmt = |i: usize| format!("{:5} {}", i + 1, lines[i]);
1268 let head = (0..lines.len().min(HEAD_LINES))
1269 .filter(|&i| !self.language.comment(lines[i]))
1270 .map(fmt)
1271 .collect::<Vec<_>>();
1272 let Some(start) = start else {
1273 let rest = (30.min(lines.len())..230.min(lines.len())).map(fmt);
1274 return head
1275 .into_iter()
1276 .chain(std::iter::once(" ...".to_owned()))
1277 .chain(rest)
1278 .collect::<Vec<_>>()
1279 .join("\n");
1280 };
1281 let indent = lines[start].len() - lines[start].trim_start().len();
1282 let mut body = Vec::new();
1283 for (i, l) in lines
1284 .iter()
1285 .copied()
1286 .enumerate()
1287 .take(start + BODY_LINES)
1288 .skip(start)
1289 {
1290 if i > start && is_test_call(l) && l.len() - l.trim_start().len() <= indent {
1291 break;
1292 }
1293 if !self.language.comment(l) {
1294 body.push(fmt(i));
1295 }
1296 }
1297 let mut out = head;
1298 if start > HEAD_LINES {
1299 out.push(" ...".into());
1300 }
1301 out.extend(body);
1302 out.join("\n")
1303 }
1304
1305 fn located_test_code(&self, test: usize) -> String {
1308 let t = &self.tests[test];
1309 let text = self.read(&t.file).unwrap_or_default();
1310 let lines = text.split('\n').collect::<Vec<_>>();
1311 let located = match self.language {
1312 Language::Ruby => ruby::locate(&text, &lines, &t.runner, &t.name),
1313 Language::Go => go::locate(&lines, &t.name),
1314 Language::Rust => rust_source::locate(&lines, &t.name),
1315 _ => jvm::locate(&lines, &t.name),
1316 };
1317 let fmt = |i: usize| format!("{:5} {}", i + 1, lines[i]);
1318 let mut open = false;
1320 let commented = lines
1321 .iter()
1322 .map(|l| {
1323 let t = l.trim();
1324 let was = open;
1325 if self.language != Language::Ruby {
1326 if !open && t.starts_with("/*") {
1327 open = true;
1328 }
1329 if open && t.contains("*/") {
1330 open = false;
1331 return true;
1332 }
1333 }
1334 was || open || self.language.comment(l)
1335 })
1336 .collect::<Vec<_>>();
1337 let head = (0..lines.len())
1339 .filter(|&i| !commented[i])
1340 .take(HEAD_LINES)
1341 .collect::<Vec<_>>();
1342 let head_end = head.last().map_or(0, |&i| i + 1);
1343 let mut out = head.iter().map(|&i| fmt(i)).collect::<Vec<_>>();
1344 let Some(located) = located else {
1345 out.push(" ...".into());
1346 out.extend((head_end..(head_end + 200).min(lines.len())).map(fmt));
1347 return out.join("\n");
1348 };
1349 let end = located
1350 .end
1351 .min(located.start + BODY_LINES)
1352 .min(lines.len().saturating_sub(1));
1353 let mut last = head_end;
1354 let mut shown = located.context.iter().copied().collect::<BTreeSet<_>>();
1355 shown.extend(located.start..=end);
1356 shown.extend(used_definitions(&lines, self.language, located.start, end));
1359 for i in shown {
1360 if i < last || (commented[i] && self.language != Language::Rust) {
1362 continue;
1363 }
1364 if i > last {
1365 out.push(" ...".into());
1366 }
1367 out.push(fmt(i));
1368 last = i + 1;
1369 }
1370 out.join("\n")
1371 }
1372
1373 fn python_test_code(&self, test: usize) -> String {
1377 let t = &self.tests[test];
1378 let text = self.read(&t.file).unwrap_or_default();
1379 let lines = text.split('\n').collect::<Vec<_>>();
1380 let title = python_title(&t.name);
1381 let indent_of = |l: &str| l.len() - l.trim_start().len();
1382 let start = (!title.is_empty())
1383 .then(|| {
1384 lines.iter().position(|l| {
1385 let t = l.trim_start();
1386 t.starts_with(&format!("def {title}("))
1387 || t.starts_with(&format!("async def {title}("))
1388 })
1389 })
1390 .flatten();
1391 let fmt = |i: usize| format!("{:5} {}", i + 1, lines[i]);
1392 let mut out = (0..lines.len().min(HEAD_LINES))
1393 .filter(|&i| !self.language.comment(lines[i]))
1394 .map(fmt)
1395 .collect::<Vec<_>>();
1396 let Some(start) = start else {
1397 out.push(" ...".into());
1398 out.extend((HEAD_LINES.min(lines.len())..(HEAD_LINES + 200).min(lines.len())).map(fmt));
1399 return out.join("\n");
1400 };
1401 let indent = indent_of(lines[start]);
1402 let mut first = start;
1403 while first > 0
1404 && lines[first - 1].trim_start().starts_with('@')
1405 && indent_of(lines[first - 1]) == indent
1406 {
1407 first -= 1;
1408 }
1409 let class = (indent > 0)
1410 .then(|| {
1411 (0..first).rev().find(|&i| {
1412 lines[i].trim_start().starts_with("class ") && indent_of(lines[i]) < indent
1413 })
1414 })
1415 .flatten();
1416 for i in class.into_iter().chain(first..start) {
1417 if i >= HEAD_LINES {
1418 if out.last().is_some_and(|l| l != " ...") {
1419 out.push(" ...".into());
1420 }
1421 out.push(fmt(i));
1422 }
1423 }
1424 if start >= HEAD_LINES
1425 && out.last().is_some_and(|l| l != " ...")
1426 && first == start
1427 && class.is_none()
1428 {
1429 out.push(" ...".into());
1430 }
1431 for (i, &l) in lines.iter().enumerate().skip(start).take(BODY_LINES) {
1432 if i > start && !l.trim().is_empty() && indent_of(l) <= indent {
1433 break;
1434 }
1435 if !self.language.comment(l) && i >= HEAD_LINES {
1436 out.push(fmt(i));
1437 }
1438 }
1439 out.join("\n")
1440 }
1441
1442 pub fn helpers(&self, test: usize, questions: usize) -> BTreeMap<String, String> {
1448 if self.language.asks_within_the_function() {
1449 return BTreeMap::new();
1450 }
1451 let cap = HELPER_CHARS.min(1500 + 400 * questions);
1452 let t = &self.tests[test];
1453 let text = self.read(&t.file).unwrap_or_default();
1454 let mut out = BTreeMap::new();
1455 let mut size = 0;
1456 let files = match self.language {
1457 Language::JavaScript | Language::Python => Vec::new(),
1458 Language::Ruby => ruby::helpers(&self.root, &t.file, &text),
1459 Language::Go => go::helpers(&self.root, &t.file, &text),
1460 Language::Rust => rust_source::helpers(&self.root, &t.file, &text),
1461 Language::Jvm => jvm::imports(&text)
1462 .iter()
1463 .filter_map(|import| {
1464 jvm::class_paths(&import.replace('/', "."))
1465 .iter()
1466 .find_map(|suffix| {
1467 self.jvm_files.iter().find(|f| ends_with_path(f, suffix))
1468 })
1469 .cloned()
1470 })
1471 .collect(),
1472 };
1473 for file in files {
1474 if out.contains_key(&file) {
1475 continue;
1476 }
1477 if self.source_files.contains(&file) || file.contains("node_modules") {
1478 continue;
1479 }
1480 let mut body = self.read(&file).map(|b| b.to_string()).unwrap_or_default();
1481 if size + body.len() > cap {
1482 let keep = cap.saturating_sub(size);
1483 let cut = body
1484 .char_indices()
1485 .map(|(i, _)| i)
1486 .take_while(|&i| i <= keep)
1487 .last()
1488 .unwrap_or(0);
1489 body = format!("{}\n... (cut)", &body[..cut]);
1490 }
1491 size += body.len();
1492 out.insert(file, numbered(&body.split('\n').collect::<Vec<_>>()));
1493 if size >= cap {
1494 break;
1495 }
1496 }
1497 if self.language == Language::Ruby {
1498 let mut used = 0;
1499 for file in ruby::support_files(&self.root, &text) {
1500 if out.contains_key(&file) || used >= FIXTURE_CHARS {
1501 continue;
1502 }
1503 let mut body = self.read(&file).map(|b| b.to_string()).unwrap_or_default();
1504 if used + body.len() > FIXTURE_CHARS {
1505 let keep = FIXTURE_CHARS - used;
1506 let cut = body
1507 .char_indices()
1508 .map(|(i, _)| i)
1509 .take_while(|&i| i <= keep)
1510 .last()
1511 .unwrap_or(0);
1512 body = format!("{}\n... (cut)", &body[..cut]);
1513 }
1514 used += body.len();
1515 out.insert(file, numbered(&body.split('\n').collect::<Vec<_>>()));
1516 }
1517 }
1518 out
1519 }
1520
1521 pub fn code_run(&self, test: usize, asked: &[usize]) -> BTreeMap<String, String> {
1532 let budget = CODE_CHARS.min(1000 + 400 * asked.len());
1533 let within = self.language.asks_within_the_function();
1534 let ran = &self.ran[test];
1535 let mut want = BTreeMap::<String, BTreeSet<usize>>::new();
1536 for &a in asked {
1537 want.entry(self.statements[a].file.clone())
1538 .or_default()
1539 .insert(self.statements[a].line);
1540 }
1541 let mut files = want.keys().cloned().collect::<Vec<_>>();
1542 let mut others = ran
1543 .iter()
1544 .filter(|(f, _)| !within && !want.contains_key(*f))
1545 .collect::<Vec<_>>();
1546 others.sort_by_key(|(f, lines)| (std::cmp::Reverse(lines.len()), (*f).clone()));
1547 files.extend(others.into_iter().map(|(f, _)| f.clone()));
1548 let empty = BTreeSet::new();
1549 let mut out = BTreeMap::new();
1550 let mut total = 0;
1551 for f in files {
1552 let Some(source) = self.read(&f) else {
1553 continue;
1554 };
1555 let text = source.split('\n').collect::<Vec<_>>();
1556 let r = ran.get(&f).unwrap_or(&empty);
1557 let code = (1..=text.len())
1558 .filter(|&i| !self.language.comment(text[i - 1]))
1559 .collect::<Vec<_>>();
1560 let pos = code
1561 .iter()
1562 .enumerate()
1563 .map(|(n, &i)| (i, n))
1564 .collect::<BTreeMap<_, _>>();
1565 let around = |l: usize, w: usize| -> Vec<usize> {
1566 match pos.get(&l) {
1567 Some(&n) => code[n.saturating_sub(w)..(n + w + 1).min(code.len())].to_vec(),
1568 None => (l.saturating_sub(w).max(1)..=(l + w).min(text.len())).collect(),
1569 }
1570 };
1571 let mut show = BTreeSet::new();
1572 for &l in r {
1573 show.extend(around(l, CONTEXT_LINES));
1574 }
1575 for &l in want.get(&f).unwrap_or(&empty) {
1576 show.extend(around(l, WINDOW_LINES));
1577 }
1578 let end = show
1581 .iter()
1582 .rev()
1583 .copied()
1584 .find(|&i| i != 0 && i <= text.len() && !self.language.comment(text[i - 1]))
1585 .unwrap_or(0);
1586 if within {
1587 let functions = outermost_functions(self.language, &f, &source);
1588 let around =
1589 |l: usize| (l.saturating_sub(OUTSIDE_A_FUNCTION), l + OUTSIDE_A_FUNCTION);
1590 let function = |l: usize| {
1591 functions
1592 .iter()
1593 .copied()
1594 .find(|&(from, to)| from <= l && l <= to)
1595 };
1596 let begins = self
1598 .statements
1599 .iter()
1600 .filter(|s| s.file == f)
1601 .map(|s| s.line)
1602 .collect::<BTreeSet<_>>();
1603 let mut kept = Vec::new();
1604 for statement in asked.iter().map(|&a| &self.statements[a]) {
1605 if statement.file != f {
1606 continue;
1607 }
1608 kept.push(function(statement.line).unwrap_or(around(statement.line)));
1609 for l in (statement.line + 1)
1613 ..statement.line + whole_statement(statement).unwrap_or(1)
1614 {
1615 kept.push(
1616 begins
1617 .contains(&l)
1618 .then(|| function(l))
1619 .flatten()
1620 .unwrap_or(around(l)),
1621 );
1622 }
1623 }
1624 show.retain(|i| kept.iter().any(|&(from, to)| from <= *i && *i <= to));
1625 }
1626 let mut lines = Vec::new();
1627 let mut last = 0;
1628 for i in show {
1629 if i == 0 || i > text.len() || self.language.comment(text[i - 1]) {
1630 continue;
1631 }
1632 if i != last + 1 {
1633 lines.push(format!(" ... {} lines not shown", i - last - 1));
1634 }
1635 lines.push(format!(
1636 "{}{:5} {}",
1637 if r.contains(&i) { '▶' } else { ' ' },
1638 i,
1639 text[i - 1]
1640 ));
1641 last = i;
1642 }
1643 if within && last != 0 && end > last {
1644 lines.push(format!(" ... {} lines not shown", end - last));
1645 }
1646 let body = lines.join("\n");
1647 if !want.contains_key(&f) && total + body.len() > budget {
1648 continue;
1649 }
1650 total += body.len();
1651 out.insert(f, body);
1652 }
1653 out
1654 }
1655
1656 pub fn questions(&self, statement: usize) -> Vec<String> {
1658 let s = &self.statements[statement];
1659 let line = s.text.lines().next().unwrap_or("").trim();
1660 let changes = match (s.change, &s.replacement) {
1661 (Change::ReturnUndefined | Change::ValueUndefined, Some(r))
1662 if self.language == Language::Rust =>
1663 {
1664 vec![rust_question(line, s.change, r)]
1665 }
1666 (Change::ReturnUndefined, Some(r)) if self.language == Language::Go => {
1669 vec![format!(
1670 "`{line}` becomes `{r}` (the original expression is still evaluated first)"
1671 )]
1672 }
1673 (Change::ValueUndefined, Some(r)) if self.language == Language::Go => {
1674 vec![format!(
1675 "`{line}` becomes `{r}` (the original value is still computed first)"
1676 )]
1677 }
1678 _ => self.default_questions(s, line),
1679 };
1680 changes
1681 .into_iter()
1682 .map(|c| format!("{}:{}: {c}.", s.file, s.line))
1683 .collect()
1684 }
1685
1686 fn default_questions(&self, s: &Statement, line: &str) -> Vec<String> {
1687 match s.change {
1688 Change::ReturnUndefined if self.language == Language::Go => vec![format!(
1691 "`{line}` returns the zero value of each result instead: nil for an error, pointer, slice, map or interface, 0, \"\" or false, or an empty struct (its expressions still run)"
1692 )],
1693 Change::ReturnUndefined
1696 if self.language == Language::Jvm
1697 && s.text.contains("return")
1698 && ["when", "switch", "try"].iter().any(|k| {
1699 line.strip_prefix(k)
1700 .is_some_and(|rest| rest.starts_with([' ', '(', '{']))
1701 }) =>
1702 {
1703 vec![format!(
1704 "every value `{line}` returns becomes {} instead (the returned expressions still run)",
1705 self.language.nothing()
1706 )]
1707 }
1708 Change::ReturnUndefined if self.language == Language::Jvm => vec![format!(
1709 "`{line}` returns {} instead (its expression still runs)",
1710 self.language.nothing()
1711 )],
1712 Change::ReturnUndefined if matches!(self.language, Language::Ruby | Language::Rust) => {
1713 let nothing = match self.language {
1714 Language::Rust
1715 if ["Ok(", "Err(", "return Ok(", "return Err("]
1716 .iter()
1717 .any(|p| line.starts_with(p)) =>
1718 {
1719 "`Ok(Default::default())`"
1720 }
1721 other => other.nothing(),
1722 };
1723 vec![if line.starts_with("return") {
1724 format!("`{line}` returns {nothing} instead (the expression is not evaluated)")
1725 } else {
1726 format!(
1729 "`{line}` becomes {nothing}: it is not evaluated, so nothing it does happens (no call it makes, no value it stores), and the enclosing block or method produces {nothing}"
1730 )
1731 }]
1732 }
1733 Change::ValueUndefined if self.language == Language::Go => vec![format!(
1734 "`{line}`: after it runs, the variables it assigns hold the zero value of their type instead"
1735 )],
1736 Change::ValueUndefined if self.language == Language::Jvm => vec![format!(
1737 "`{line}`: after it runs, the variable it assigns holds {} instead",
1738 self.language.nothing()
1739 )],
1740 Change::ValueUndefined if matches!(self.language, Language::Ruby | Language::Rust) => {
1741 let nothing = self.language.nothing();
1742 vec![match split_initializer(line) {
1743 Some(head) => format!(
1744 "`{line}`: the value after `{head}` becomes {nothing} (it is not evaluated)"
1745 ),
1746 None => format!("`{line}`: the assigned value becomes {nothing}"),
1747 }]
1748 }
1749 Change::ReturnUndefined if self.language == Language::Python => vec![format!(
1750 "`{line}` becomes `return None` (the expression is not evaluated)"
1751 )],
1752 Change::ReturnUndefined => vec![format!(
1753 "`{line}` becomes `return undefined;` (the expression is not evaluated)"
1754 )],
1755 Change::ValueUndefined if self.language == Language::Python => {
1756 vec![match split_initializer(line) {
1757 Some(head) => {
1758 format!("`{line}` becomes `{head} None` (the value is not evaluated)")
1759 }
1760 None => format!("`{line}`: the assigned value becomes None"),
1761 }]
1762 }
1763 Change::ValueUndefined => vec![match split_initializer(line) {
1764 Some(head) => format!(
1765 "`{line}` becomes `{head} undefined` (the initializer is not evaluated)"
1766 ),
1767 None => format!("`{line}`: the declared value becomes undefined"),
1768 }],
1769 Change::Skip => {
1770 let word = line.trim_end_matches(';').trim();
1771 vec![match word {
1772 "break" => format!(
1773 "`{line}` is deleted: execution does not leave the enclosing loop or switch here, and carries on with what follows it"
1774 ),
1775 "continue" | "next" => format!(
1776 "`{line}` is deleted: the rest of the loop body runs in this iteration"
1777 ),
1778 _ if whole_statement(s).is_some() => {
1783 let lines = s.text.lines().count();
1784 format!(
1785 "the whole statement starting `{line}` is deleted, all {lines} lines of it (lines {} to {}): none of it runs",
1786 s.line,
1787 s.line + lines - 1
1788 )
1789 }
1790 _ if self.language == Language::Python
1796 && (word == "return" || word.starts_with("return ")) =>
1797 {
1798 format!(
1799 "`{line}` is deleted: the function does not return here, and execution carries on with whatever follows it"
1800 )
1801 }
1802 _ if self.language == Language::Python
1803 && (word == "raise" || word.starts_with("raise ")) =>
1804 {
1805 format!(
1806 "`{line}` is deleted: no error is raised here, and execution carries on with whatever follows it"
1807 )
1808 }
1809 _ => format!("`{line}` is deleted (it never runs)"),
1810 }]
1811 }
1812 Change::ExpressionUndefined => vec![format!(
1813 "the expression `{line}` evaluates to undefined instead (it is not evaluated)"
1814 )],
1815 Change::Flip => vec![format!("`{line}` becomes `{}`", flipped(line))],
1816 Change::BareError => vec![format!(
1817 "`{line}` throws `new Error()` instead: execution still stops here, but the error has no message and is not its own type"
1818 )],
1819 Change::Invert => {
1820 let c = match self.language {
1821 Language::Python => python_condition(line),
1822 Language::JavaScript => condition(line),
1823 Language::Ruby => ruby::condition(line),
1824 Language::Go => go::condition(line),
1825 Language::Rust => rust_source::condition(line),
1826 Language::Jvm => jvm::condition(line),
1827 };
1828 if c.starts_with("let ") {
1831 return vec![format!(
1832 "`{line}`: whenever the pattern `{c}` matches it is treated as not matching (the branch it would enter is skipped)"
1833 )];
1834 }
1835 vec![
1836 format!(
1837 "`{line}`: whenever the condition `{c}` is true it is treated as false (the branch it would enter is skipped)"
1838 ),
1839 format!(
1840 "`{line}`: whenever the condition `{c}` is false it is treated as true (the branch is entered)"
1841 ),
1842 ]
1843 }
1844 }
1845 }
1846
1847 pub fn request(&self, test: usize, asked: &[usize], model: &str) -> Value {
1849 let t = &self.tests[test];
1850 let mut questions = serde_json::Map::new();
1851 for (n, &a) in asked.iter().enumerate() {
1852 for (j, q) in self.questions(a).into_iter().enumerate() {
1853 questions.insert(
1854 format!("q{n}_{j}"),
1855 json!({"type": "noul", "instructions": {"task": q}, "criteria": {"true": "The test fails.", "false": "The test still passes."}}),
1856 );
1857 }
1858 }
1859 let mut state = json!({
1860 "instructions": INSTRUCTIONS, "runtime": runtime(&t.runner),
1861 "test": {"file": t.file, "name": t.name},
1862 "test_code": self.test_code(test),
1863 "code_run": self.code_run(test, asked)});
1864 if !self.language.asks_within_the_function() {
1865 state["helpers"] = json!(self.helpers(test, asked.len()));
1866 }
1867 json!({"model": model, "state": state, "questions": questions})
1868 }
1869
1870 pub fn pair_key(&self, statement: usize, test: usize, model: &str, salt: &str) -> String {
1875 let t = &self.tests[test];
1876 let parts = json!([
1877 model,
1878 salt,
1879 INSTRUCTIONS,
1880 runtime(&t.runner),
1881 true,
1882 &t.file,
1883 &t.name,
1884 strip_numbers(&self.test_code(test)),
1885 self.helpers(test, 1)
1886 .into_iter()
1887 .map(|(f, b)| (f, strip_numbers(&b)))
1888 .collect::<BTreeMap<_, _>>(),
1889 self.code_run(test, &[statement])
1890 .into_iter()
1891 .map(|(f, b)| (f, strip_numbers(&b)))
1892 .collect::<BTreeMap<_, _>>(),
1893 self.questions(statement)
1894 .iter()
1895 .map(|q| strip_location(q))
1896 .collect::<Vec<_>>(),
1897 ]);
1898 format!(
1899 "{:x}",
1900 Sha256::digest(serde_json::to_vec(&parts).expect("serializable key"))
1901 )
1902 }
1903}
1904
1905fn flipped(line: &str) -> String {
1908 let cut = comment_start(line);
1911 let (code, comment) = line.split_at(cut);
1912 format!("{}{comment}", flipped_code(code))
1913}
1914
1915fn comment_start(line: &str) -> usize {
1918 let bytes = line.as_bytes();
1919 let mut quote = None;
1920 let mut i = 0;
1921 while i < bytes.len() {
1922 let c = bytes[i];
1923 match quote {
1924 Some(_) if c == b'\\' => i += 1,
1925 Some(q) if c == q => quote = None,
1926 Some(_) => {}
1927 None if matches!(c, b'\'' | b'"' | b'`') => quote = Some(c),
1928 None if c == b'/' && bytes.get(i + 1) == Some(&b'/') => return i,
1929 None if c == b'#' && bytes.get(i + 1).is_none_or(|n| *n == b' ') => return i,
1930 None => {}
1931 }
1932 i += 1;
1933 }
1934 line.len()
1935}
1936
1937fn flipped_code(line: &str) -> String {
1938 let words = [
1939 ("true", "false"),
1940 ("false", "true"),
1941 ("True", "False"),
1942 ("False", "True"),
1943 ];
1944 let found = words
1945 .iter()
1946 .filter_map(|(from, to)| {
1947 line.match_indices(from)
1948 .filter(|(at, _)| {
1949 let before = line[..*at].chars().next_back();
1950 let after = line[at + from.len()..].chars().next();
1951 let word = |c: Option<char>| c.is_some_and(|c| c.is_alphanumeric() || c == '_');
1952 !word(before) && !word(after)
1953 })
1954 .last()
1955 .map(|(at, _)| (at, *from, *to))
1956 })
1957 .max_by_key(|(at, _, _)| *at);
1958 match found {
1959 Some((at, from, to)) => format!("{}{to}{}", &line[..at], &line[at + from.len()..]),
1960 None => line.to_owned(),
1961 }
1962}
1963
1964fn rust_question(line: &str, change: Change, replacement: &str) -> String {
1966 let repl = replacement.split_whitespace().collect::<Vec<_>>().join(" ");
1967 match change {
1968 Change::ReturnUndefined if line.starts_with("return") => {
1969 format!("`{line}` becomes `return {repl};`")
1970 }
1971 Change::ReturnUndefined => {
1972 format!("`{line}` becomes `{repl}` (it is the value the enclosing block produces)")
1973 }
1974 _ => match split_initializer(line) {
1975 Some(head) => format!("`{line}` becomes `{head} {repl};`"),
1976 None => format!("`{line}`: the value it assigns becomes `{repl}`"),
1977 },
1978 }
1979}
1980
1981pub fn save_snapshot(path: &Path, files: &BTreeMap<String, String>) -> Result<(), String> {
1983 use std::io::Write;
1984 let bytes = serde_json::to_vec(files).map_err(|e| e.to_string())?;
1985 let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
1986 encoder.write_all(&bytes).map_err(|e| e.to_string())?;
1987 let compressed = encoder.finish().map_err(|e| e.to_string())?;
1988 let temp = path.with_extension("tmp");
1989 std::fs::write(&temp, compressed).map_err(|e| e.to_string())?;
1990 std::fs::rename(&temp, path).map_err(|e| e.to_string())
1991}
1992
1993pub fn load_snapshot(path: &Path) -> Option<BTreeMap<String, String>> {
1995 use std::io::Read;
1996 let compressed = std::fs::read(path).ok()?;
1997 let mut text = Vec::new();
1998 flate2::read::GzDecoder::new(compressed.as_slice())
1999 .read_to_end(&mut text)
2000 .ok()?;
2001 serde_json::from_slice(&text).ok()
2002}
2003
2004struct Located {
2007 context: Vec<usize>,
2008 start: usize,
2009 end: usize,
2010}
2011
2012fn indent(line: &str) -> usize {
2013 line.len() - line.trim_start().len()
2014}
2015
2016fn brace_end(lines: &[&str], start: usize) -> usize {
2019 let mut depth = 0i64;
2020 let mut opened = false;
2021 for (i, line) in lines.iter().enumerate().skip(start).take(BODY_LINES) {
2022 let mut chars = line.chars().peekable();
2023 let mut quote: Option<char> = None;
2024 while let Some(c) = chars.next() {
2025 match (quote, c) {
2026 (Some(_), '\\') => {
2027 chars.next();
2028 }
2029 (Some(q), c) if c == q => quote = None,
2030 (Some(_), _) => {}
2031 (None, '"' | '`') => quote = Some(c),
2032 (None, '/') if chars.peek() == Some(&'/') => break,
2033 (None, '{') => {
2034 depth += 1;
2035 opened = true;
2036 }
2037 (None, '}') => depth -= 1,
2038 _ => {}
2039 }
2040 }
2041 if opened && depth <= 0 {
2042 return i;
2043 }
2044 }
2045 (start + BODY_LINES).min(lines.len()).saturating_sub(1)
2046}
2047
2048fn keyword_end(lines: &[&str], start: usize) -> usize {
2050 let first = lines[start].trim_end();
2051 if first.ends_with(" end") || first.ends_with('}') {
2052 return start;
2053 }
2054 let depth = indent(lines[start]);
2055 (start + 1..lines.len().min(start + BODY_LINES))
2056 .find(|&i| {
2057 let t = lines[i].trim_start();
2058 indent(lines[i]) == depth && (t == "end" || t.starts_with("end ") || t.starts_with('}'))
2059 })
2060 .unwrap_or((start + BODY_LINES).min(lines.len()).saturating_sub(1))
2061}
2062
2063const DEFINITION_LINES: usize = 120;
2064
2065fn used_definitions(lines: &[&str], language: Language, start: usize, end: usize) -> Vec<usize> {
2068 let words = |range: std::ops::RangeInclusive<usize>| {
2069 let mut out = BTreeSet::new();
2070 for i in range {
2071 let line = lines.get(i).copied().unwrap_or("");
2072 out.extend(
2073 line.split(|c: char| !(c.is_alphanumeric() || c == '_'))
2074 .filter(|w| !w.is_empty())
2075 .map(str::to_owned),
2076 );
2077 }
2078 out
2079 };
2080 let definitions = definitions(lines, language);
2081 let mut wanted = words(start..=end);
2082 let mut picked = BTreeSet::new();
2083 let mut budget = DEFINITION_LINES;
2084 for _ in 0..2 {
2085 let mut next = BTreeSet::new();
2086 for (name, a, b) in &definitions {
2087 let (a, b) = (*a, *b);
2088 if picked.contains(&a) || (a >= start && a <= end) || !wanted.contains(name) {
2089 continue;
2090 }
2091 let size = b - a + 1;
2092 if size > budget {
2093 continue;
2094 }
2095 budget -= size;
2096 picked.insert(a);
2097 next.extend(words(a..=b));
2098 picked.extend(a..=b);
2099 }
2100 wanted = next;
2101 }
2102 picked.into_iter().collect()
2103}
2104
2105fn definitions(lines: &[&str], language: Language) -> Vec<(String, usize, usize)> {
2108 let ident = |s: &str| {
2109 s.chars()
2110 .take_while(|c| c.is_alphanumeric() || *c == '_' || *c == '?' || *c == '!')
2111 .collect::<String>()
2112 };
2113 let mut out = Vec::new();
2114 for (i, line) in lines.iter().enumerate() {
2115 let t = line.trim_start();
2116 let name = match language {
2117 Language::Go => t
2118 .strip_prefix("func ")
2119 .map(|r| match r.strip_prefix('(') {
2120 Some(r) => r.split_once(')').map_or("", |(_, r)| r.trim_start()),
2121 None => r,
2122 })
2123 .or_else(|| t.strip_prefix("var "))
2124 .or_else(|| t.strip_prefix("type "))
2125 .filter(|_| indent(line) == 0)
2126 .map(ident),
2127 Language::Rust => {
2128 let r = t.strip_prefix("pub ").unwrap_or(t);
2129 let r = r.strip_prefix("pub(crate) ").unwrap_or(r);
2130 ["fn ", "const ", "static ", "struct ", "enum "]
2131 .iter()
2132 .find_map(|k| r.strip_prefix(k))
2133 .map(ident)
2134 }
2135 Language::Ruby => t
2136 .strip_prefix("def ")
2137 .map(|r| ident(r.strip_prefix("self.").unwrap_or(r)))
2138 .or_else(|| {
2139 ["let(:", "let!(:", "subject(:"]
2140 .iter()
2141 .find_map(|k| t.strip_prefix(k))
2142 .map(ident)
2143 })
2144 .or_else(|| {
2145 (t.starts_with("subject ") || t.starts_with("subject{"))
2146 .then(|| "subject".into())
2147 }),
2148 Language::Jvm => {
2149 if let Some(r) = t.split_once("fun ").filter(|(head, _)| !head.contains('(')) {
2150 Some(ident(r.1.trim_start_matches('`')))
2151 } else if t.contains('(')
2152 && !t.ends_with(';')
2153 && !t.starts_with(['@', '}', '.', '/', '*', '+', '"'])
2154 && ![
2155 "return", "new ", "if", "for", "while", "switch", "catch", "else", "try",
2156 "throw", "assert", "super", "this",
2157 ]
2158 .iter()
2159 .any(|k| t.starts_with(k))
2160 {
2161 let head = t.split('(').next().unwrap_or("");
2162 let mut parts = head.split_whitespace().rev();
2163 let name = parts.next().unwrap_or("");
2164 (parts.next().is_some() && !head.contains('=')).then(|| ident(name))
2165 } else {
2166 None
2167 }
2168 }
2169 _ => None,
2170 };
2171 let Some(name) = name.filter(|n| !n.is_empty()) else {
2172 continue;
2173 };
2174 let end = match language {
2175 Language::Ruby => keyword_end(lines, i),
2176 _ if line.contains('{')
2177 || lines
2178 .get(i + 1)
2179 .is_some_and(|n| n.trim_start().starts_with('{')) =>
2180 {
2181 brace_end(lines, i)
2182 }
2183 _ => i,
2184 };
2185 out.push((name, i, end));
2186 }
2187 out
2188}
2189
2190fn ends_with_path(path: &str, suffix: &str) -> bool {
2192 path.strip_suffix(suffix)
2193 .is_some_and(|head| head.is_empty() || head.ends_with('/'))
2194}
2195
2196fn project_files(root: &Path, extensions: &[&str]) -> Vec<String> {
2199 let mut out = ignore::WalkBuilder::new(root)
2200 .build()
2201 .filter_map(Result::ok)
2202 .filter(|e| e.file_type().is_some_and(|t| t.is_file()))
2203 .filter(|e| {
2204 e.path()
2205 .extension()
2206 .and_then(|x| x.to_str())
2207 .is_some_and(|x| extensions.contains(&x))
2208 })
2209 .filter_map(|e| {
2210 e.path()
2211 .strip_prefix(root)
2212 .ok()
2213 .map(|p| p.to_string_lossy().replace('\\', "/"))
2214 })
2215 .filter(|p| !p.starts_with(".supercov/"))
2216 .collect::<Vec<_>>();
2217 out.sort();
2218 out
2219}
2220
2221fn is_test_call(line: &str) -> bool {
2222 let t = line.trim_start();
2223 let rest = if let Some(r) = t.strip_prefix("it") {
2224 r
2225 } else if let Some(r) = t.strip_prefix("test") {
2226 r
2227 } else {
2228 return false;
2229 };
2230 let rest = if let Some(r) = rest.strip_prefix('.') {
2231 r.trim_start_matches(|c: char| c.is_alphanumeric() || c == '_')
2232 } else {
2233 rest
2234 };
2235 rest.trim_start().starts_with('(')
2236}
2237
2238fn split_initializer(line: &str) -> Option<String> {
2240 let b = line.as_bytes();
2241 for i in 1..b.len() {
2242 if b[i] == b'='
2243 && !matches!(b[i - 1], b'=' | b'!' | b'<' | b'>')
2244 && !matches!(b.get(i + 1), Some(b'=') | Some(b'>'))
2245 {
2246 return Some(line[..=i].to_owned());
2247 }
2248 }
2249 None
2250}
2251
2252fn python_title(name: &str) -> &str {
2256 let last = name.rsplit("::").next().unwrap_or(name);
2257 let last = last.split('[').next().unwrap_or(last);
2258 last.rsplit('.').next().unwrap_or(last)
2259}
2260
2261fn python_condition(line: &str) -> String {
2262 let t = line.trim();
2263 let t = t
2264 .strip_prefix("elif")
2265 .or_else(|| t.strip_prefix("if"))
2266 .map(str::trim_start)
2267 .unwrap_or(t);
2268 t.strip_suffix(':')
2269 .map(str::trim_end)
2270 .unwrap_or(t)
2271 .to_owned()
2272}
2273
2274fn condition(line: &str) -> String {
2276 let mut t = line.trim();
2277 if let Some(r) = t.strip_prefix('}') {
2278 let r = r.trim_start();
2279 if let Some(r) = r.strip_prefix("else") {
2280 t = r.trim_start();
2281 }
2282 }
2283 let Some(r) = t.strip_prefix("if") else {
2284 return line.to_owned();
2285 };
2286 let r = r.trim_start();
2287 let Some(r) = r.strip_prefix('(') else {
2288 return line.to_owned();
2289 };
2290 let r = r.trim_end();
2291 let r = r.strip_suffix('{').map(str::trim_end).unwrap_or(r);
2292 match r.strip_suffix(')') {
2293 Some(c) => c.to_owned(),
2294 None => line.to_owned(),
2295 }
2296}
2297
2298fn strip_numbers(text: &str) -> String {
2299 text.split('\n')
2300 .map(|l| {
2301 let (mark, rest) = match l.chars().next() {
2302 Some(c @ ('▶' | ' ')) => (Some(c), &l[c.len_utf8()..]),
2303 _ => (None, l),
2304 };
2305 let trimmed = rest.trim_start();
2306 let digits = trimmed.len()
2307 - trimmed
2308 .trim_start_matches(|c: char| c.is_ascii_digit())
2309 .len();
2310 if digits > 0 && trimmed[digits..].starts_with(' ') {
2311 format!(
2312 "{}{}",
2313 mark.map(String::from).unwrap_or_default(),
2314 &trimmed[digits + 1..]
2315 )
2316 } else if let Some(n) = l
2317 .trim_start()
2318 .strip_prefix("... ")
2319 .and_then(|r| r.split_once(" lines not shown"))
2320 {
2321 let _ = n;
2322 "...".to_owned()
2323 } else {
2324 l.to_owned()
2325 }
2326 })
2327 .collect::<Vec<_>>()
2328 .join("\n")
2329}
2330
2331fn strip_location(question: &str) -> String {
2332 match question.split_once(": ") {
2334 Some((loc, rest)) => match loc.rsplit_once(':') {
2335 Some((file, line)) if line.chars().all(|c| c.is_ascii_digit()) => {
2336 format!("{file}:: {rest}")
2337 }
2338 _ => question.to_owned(),
2339 },
2340 None => question.to_owned(),
2341 }
2342}
2343
2344fn normalize(path: &Path) -> String {
2348 let mut out = Vec::new();
2349 for c in path.components() {
2350 match c {
2351 std::path::Component::ParentDir => {
2352 out.pop();
2353 }
2354 std::path::Component::CurDir => {}
2355 other => out.push(other.as_os_str().to_string_lossy().into_owned()),
2356 }
2357 }
2358 out.join("/")
2359}
2360
2361#[cfg(test)]
2362mod tests {
2363 use super::*;
2364
2365 #[test]
2366 fn a_statement_many_tests_run_is_asked_of_the_tests_that_ran_little_else() {
2367 let source = (0..10)
2372 .map(|n| format!("const v{n} = compute({n});"))
2373 .collect::<Vec<_>>()
2374 .join("\n");
2375 let test = |file: &str, name: &str| InputTest {
2376 id: format!("{file}::{name}"),
2377 file: Some(file.to_owned()),
2378 name: name.to_owned(),
2379 role: "test".into(),
2380 runner: "node:test".into(),
2381 };
2382 let mut tests = (0..17)
2383 .map(|n| {
2384 let name = if n == 16 {
2385 "calls compute on start"
2386 } else {
2387 "serves a request"
2388 };
2389 test(&format!("tests/e2e{n:02}.test.js"), name)
2390 })
2391 .collect::<Vec<_>>();
2392 tests.extend((0..3).map(|n| test("tests/zz_unit.test.js", &format!("total case {n}"))));
2393 let everyone = (0..20).collect::<Vec<u32>>();
2394 let end_to_end = (0..17).collect::<Vec<u32>>();
2395 let points = (0..10)
2396 .map(|n| InputPoint {
2397 file: "src/sum.js".into(),
2398 line: n + 1,
2399 source: format!("const v{n} = compute({n});"),
2400 tests: if n == 0 {
2401 everyone.clone()
2402 } else {
2403 end_to_end.clone()
2404 },
2405 })
2406 .collect();
2407 let input = AssessmentInput {
2408 tests,
2409 points,
2410 decisions: Vec::new(),
2411 };
2412 let sources = BTreeMap::from([("src/sum.js".to_owned(), source)]);
2413 let population = population_from(
2414 Path::new("/nonexistent"),
2415 &input,
2416 &sources,
2417 Language::JavaScript,
2418 )
2419 .unwrap();
2420 let target = population
2421 .statements
2422 .iter()
2423 .position(|s| s.line == 1)
2424 .unwrap();
2425 let unit = |order: &[usize]| {
2426 order
2427 .iter()
2428 .filter(|&&t| population.tests[t].file == "tests/zz_unit.test.js")
2429 .count()
2430 };
2431 let spread = population.order(target, MORE_TESTS);
2432 assert_eq!(
2433 unit(&spread),
2434 0,
2435 "file order alone leaves the unit tests out"
2436 );
2437 let last = population.last_order(target, &spread, LAST_TESTS);
2438 assert_eq!(last.len(), LAST_TESTS);
2439 assert!(last.iter().all(|t| !spread.contains(t)), "{last:?}");
2440 assert_eq!(population.tests[last[0]].name, "calls compute on start");
2443 assert_eq!(unit(&last[1..]), 2, "{last:?}");
2444 assert_eq!(
2445 name_words("const hostCount = getHTTPResponse2(send_data)"),
2446 [
2447 "const",
2448 "count",
2449 "data",
2450 "host",
2451 "http",
2452 "response2",
2453 "send"
2454 ]
2455 .map(String::from)
2456 .into()
2457 );
2458 }
2459
2460 #[test]
2461 fn a_statement_is_shown_with_the_function_it_is_in() {
2462 let typescript = "import { log } from './log'\n\nexport const seal = (key: string) => {\n const parts = key.split('-').map((part) => {\n return part.trim()\n })\n return parts\n}\n\nclass Vault {\n size = 0\n @logged()\n open(door: string) {\n return () => door\n }\n}\n\nserver.on('close', function () {\n log('closed')\n})\n\nif (flag) {\n function inner() {\n return 1\n }\n}\nconst limit = 4\n";
2465 assert_eq!(
2466 outermost_functions(Language::JavaScript, "src/vault.ts", typescript),
2467 [(3, 8), (12, 15), (18, 20), (23, 25)]
2468 );
2469 let python = "import os\n\nLIMIT = 4\n\n@cached\ndef seal(key):\n def trim(part):\n return part.strip()\n return [trim(part) for part in key.split('-')]\n\nclass Vault:\n size = 0\n\n async def open(self, door):\n return lambda: door\n\nif os.name == 'nt':\n def separator():\n return ';'\n";
2470 assert_eq!(
2471 outermost_functions(Language::Python, "vault.py", python),
2472 [(5, 9), (14, 15), (18, 19)]
2473 );
2474 assert!(outermost_functions(Language::Go, "vault.go", "package vault\n").is_empty());
2476 assert!(Language::Python.asks_within_the_function());
2477 assert!(!Language::Ruby.asks_within_the_function());
2478 }
2479
2480 #[test]
2481 fn changes_follow_the_audit() {
2482 let source = "import x from 'y'\nexport const a = 1\nlet b: number\nfunction f() {\n if (a) return 2\n return;\n}\nconst el = <div>{a && b}</div>\n";
2483 let starts = statement_starts("x.tsx", source);
2484 let js = Language::JavaScript;
2485 assert_eq!(change_at(&starts, 1, "import x from 'y'", js), None);
2486 assert_eq!(
2487 change_at(&starts, 2, "const a = 1", js),
2488 Some(Change::ValueUndefined)
2489 );
2490 assert_eq!(change_at(&starts, 3, "let b: number", js), None);
2491 assert_eq!(
2492 change_at(&starts, 5, "if (a) return 2", js),
2493 Some(Change::Invert)
2494 );
2495 assert_eq!(change_at(&starts, 6, "return;", js), Some(Change::Skip));
2496 assert_eq!(
2497 change_at(&starts, 8, "a && b", js),
2498 Some(Change::ExpressionUndefined)
2499 );
2500 }
2501
2502 #[test]
2503 fn a_change_that_cannot_change_anything_is_not_asked() {
2504 let source = "class H extends Map {\n constructor() {\n super([])\n }\n}\nfunction f(a) {\n if (a) return undefined\n if (!a) return void 0\n let answered = false\n const done = true, more = 1\n return false\n}\n";
2508 let starts = statement_starts("x.ts", source);
2509 let js = Language::JavaScript;
2510 assert_eq!(change_at(&starts, 3, "super([])", js), None);
2511 assert_eq!(
2512 change_at(&starts, 7, "return undefined", js),
2513 Some(Change::Skip)
2514 );
2515 assert_eq!(
2516 change_at(&starts, 8, "return void 0", js),
2517 Some(Change::Skip)
2518 );
2519 let typed = "function f<T>(a: number): T {\n if (a === 1) return undefined as T\n if (a === 2) return (undefined as unknown) as T\n if (a === 3) return <T>undefined\n return undefined!\n}\n";
2522 let starts = statement_starts("x.ts", typed);
2523 for (line, text) in [
2524 (2, "return undefined as T"),
2525 (3, "return (undefined as unknown) as T"),
2526 (4, "return <T>undefined"),
2527 (5, "return undefined!"),
2528 ] {
2529 assert_eq!(
2530 change_at(&starts, line, text, js),
2531 Some(Change::Skip),
2532 "{text}"
2533 );
2534 }
2535 let starts = statement_starts("x.ts", source);
2536 assert_eq!(
2537 change_at(&starts, 9, "let answered = false", js),
2538 Some(Change::Flip)
2539 );
2540 assert_eq!(
2542 change_at(&starts, 10, "const done = true, more = 1", js),
2543 Some(Change::ValueUndefined)
2544 );
2545 assert_eq!(
2546 change_at(&starts, 11, "return false", js),
2547 Some(Change::Flip)
2548 );
2549 let python = python_statement_starts(
2550 "def f(a):\n if a:\n return None\n ready = False\n return True\n",
2551 );
2552 let py = Language::Python;
2553 assert_eq!(change_at(&python, 3, "return None", py), Some(Change::Skip));
2554 assert_eq!(
2555 change_at(&python, 4, "ready = False", py),
2556 Some(Change::Flip)
2557 );
2558 assert_eq!(change_at(&python, 5, "return True", py), Some(Change::Flip));
2559 assert_eq!(flipped("let answered = false"), "let answered = true");
2560 assert_eq!(flipped("return isTrue && false;"), "return isTrue && true;");
2561 assert_eq!(flipped("ready = False"), "ready = True");
2562 assert_eq!(
2563 flipped("let ready = false // true once open"),
2564 "let ready = true // true once open"
2565 );
2566 assert_eq!(
2567 flipped("ready = False # True later"),
2568 "ready = True # True later"
2569 );
2570 assert_eq!(flipped("this.#open = false"), "this.#open = true");
2571 assert_eq!(
2572 flipped("const s = 'a // b', on = false"),
2573 "const s = 'a // b', on = true"
2574 );
2575 }
2576
2577 #[test]
2578 fn a_typescript_throw_keeps_its_types() {
2579 let source = "async function list(reply) {\n const result = await reply\n if ('error' in result)\n throw new Error(`tools/list failed: ${result.error.message}`)\n return result.result\n}\n";
2582 let typed = statement_starts("x.ts", source);
2583 let js = Language::JavaScript;
2584 assert_eq!(
2585 change_at(
2586 &typed,
2587 4,
2588 "throw new Error(`tools/list failed: ${result.error.message}`)",
2589 js
2590 ),
2591 Some(Change::BareError)
2592 );
2593 let plain = statement_starts("x.js", source);
2594 assert_eq!(
2595 change_at(
2596 &plain,
2597 4,
2598 "throw new Error(`tools/list failed: ${result.error.message}`)",
2599 js
2600 ),
2601 Some(Change::Skip)
2602 );
2603 }
2604
2605 #[test]
2606 fn a_deleted_statement_says_what_goes_and_what_then_runs() {
2607 let source = "def f(items, n):\n if n < 0:\n raise ValueError(\"negative\")\n if not items:\n return None\n for item in items:\n use(item)\n log(n)\n return n\n";
2608 let point = |line: usize, text: &str| InputPoint {
2609 file: "pkg/f.py".into(),
2610 line,
2611 source: text.into(),
2612 tests: vec![0],
2613 };
2614 let input = AssessmentInput {
2615 tests: vec![InputTest {
2616 id: "tests/test_f.py::test_f".into(),
2617 file: Some("tests/test_f.py".into()),
2618 name: "tests/test_f.py::test_f".into(),
2619 role: "test".into(),
2620 runner: "pytest".into(),
2621 }],
2622 points: vec![
2623 point(3, "raise ValueError(\"negative\")"),
2624 point(5, "return None"),
2625 point(6, "for item in items:\n use(item)"),
2626 point(8, "log(n)"),
2627 ],
2628 decisions: Vec::new(),
2629 };
2630 let sources = BTreeMap::from([("pkg/f.py".to_owned(), source.to_owned())]);
2631 let python = population_from(
2632 Path::new("/nonexistent"),
2633 &input,
2634 &sources,
2635 Language::Python,
2636 )
2637 .unwrap();
2638 let question = |population: &Population, line: usize| {
2639 let at = population
2640 .statements
2641 .iter()
2642 .position(|s| s.line == line)
2643 .unwrap();
2644 population.questions(at).join(" | ")
2645 };
2646 assert_eq!(
2647 question(&python, 3),
2648 "pkg/f.py:3: `raise ValueError(\"negative\")` is deleted: no error is raised here, and execution carries on with whatever follows it."
2649 );
2650 assert_eq!(
2651 question(&python, 5),
2652 "pkg/f.py:5: `return None` is deleted: the function does not return here, and execution carries on with whatever follows it."
2653 );
2654 assert_eq!(
2655 question(&python, 6),
2656 "pkg/f.py:6: the whole statement starting `for item in items:` is deleted, all 2 lines of it (lines 6 to 7): none of it runs."
2657 );
2658 assert_eq!(
2659 question(&python, 8),
2660 "pkg/f.py:8: `log(n)` is deleted (it never runs)."
2661 );
2662 let js = AssessmentInput {
2665 tests: vec![InputTest {
2666 id: "t".into(),
2667 file: Some("test/g.test.js".into()),
2668 name: "g".into(),
2669 role: "test".into(),
2670 runner: "node:test".into(),
2671 }],
2672 points: vec![InputPoint {
2673 file: "src/g.js".into(),
2674 line: 3,
2675 source: "return;".into(),
2676 tests: vec![0],
2677 }],
2678 decisions: Vec::new(),
2679 };
2680 let sources = BTreeMap::from([(
2681 "src/g.js".to_owned(),
2682 "function g(a) {\n if (a) {\n return;\n }\n use(a);\n}\n".to_owned(),
2683 )]);
2684 let javascript = population_from(
2685 Path::new("/nonexistent"),
2686 &js,
2687 &sources,
2688 Language::JavaScript,
2689 )
2690 .unwrap();
2691 assert_eq!(
2692 question(&javascript, 3),
2693 "src/g.js:3: `return;` is deleted (it never runs)."
2694 );
2695 }
2696
2697 #[test]
2698 fn a_statement_s_text_is_what_a_test_would_quote() {
2699 assert_eq!(
2700 quoted_fragments("logger.info(` - Headers: ${describeHeaders(headers)}`)"),
2701 vec!["- Headers:"]
2702 );
2703 assert_eq!(
2704 quoted_fragments("logger.error(`Child exited: code=${code}, signal=${signal}`)"),
2705 vec![", signal=", "Child exited: code="]
2706 );
2707 assert_eq!(
2708 quoted_fragments("log(f\"Serving {path} on {port}\")"),
2709 vec!["Serving"]
2710 );
2711 assert!(quoted_fragments("x = 'abc' + \"1234567\"").is_empty());
2713 assert!(quoted_fragments("return a + b").is_empty());
2714 }
2715
2716 #[test]
2717 fn python_changes_follow_the_audit() {
2718 let source = "import os\n\n\ndef f(x):\n \"\"\"Docs.\"\"\"\n y = x + 1\n if y > 2:\n return y\n z: int\n count += 1\n return\ndef g(v):\n if v:\n pass\n elif v is None:\n pass\n";
2719 let starts = python_statement_starts(source);
2720 let py = Language::Python;
2721 assert_eq!(change_at(&starts, 1, "import os", py), None);
2722 assert_eq!(change_at(&starts, 4, "def f(x):", py), None);
2723 assert_eq!(change_at(&starts, 5, "\"\"\"Docs.\"\"\"", py), None);
2724 assert_eq!(
2725 change_at(&starts, 6, "y = x + 1", py),
2726 Some(Change::ValueUndefined)
2727 );
2728 assert_eq!(change_at(&starts, 7, "if y > 2:", py), Some(Change::Invert));
2729 assert_eq!(
2730 change_at(&starts, 8, "return y", py),
2731 Some(Change::ReturnUndefined)
2732 );
2733 assert_eq!(change_at(&starts, 9, "z: int", py), None);
2734 assert_eq!(change_at(&starts, 10, "count += 1", py), Some(Change::Skip));
2735 assert_eq!(change_at(&starts, 11, "return", py), Some(Change::Skip));
2736 assert_eq!(
2737 change_at(&starts, 15, "v is None", py),
2738 Some(Change::Invert)
2739 );
2740 assert_eq!(
2741 python_title("tests/test_a.py::TestA::test_b[1.5-x]"),
2742 "test_b"
2743 );
2744 assert_eq!(python_title("tests.test_a.TestA.test_b"), "test_b");
2745 assert_eq!(python_condition("if y > 2:"), "y > 2");
2746 assert_eq!(python_condition("elif v is None:"), "v is None");
2747 }
2748
2749 #[test]
2750 fn change_texts() {
2751 assert_eq!(
2752 split_initializer("const x = a === b").as_deref(),
2753 Some("const x =")
2754 );
2755 assert_eq!(
2756 split_initializer("const f = () => 1").as_deref(),
2757 Some("const f =")
2758 );
2759 assert_eq!(condition("} else if (x > 1) {"), "x > 1");
2760 assert_eq!(
2761 condition("if (!body) return null"),
2762 "if (!body) return null"
2763 );
2764 assert!(is_test_call(" it(\"works\", () => {"));
2765 assert!(is_test_call("test.each([1])('x', () => {"));
2766 assert!(!is_test_call("items.push(1)"));
2767 }
2768
2769 #[test]
2770 fn keys_ignore_line_numbers() {
2771 assert_eq!(
2772 strip_numbers("▶ 12 return x\n ... 3 lines not shown\n 15 }"),
2773 "▶ return x\n...\n }"
2774 );
2775 assert_eq!(
2776 strip_location("src/a.ts:41: `x` is deleted."),
2777 "src/a.ts:: `x` is deleted."
2778 );
2779 }
2780}