use bynk_check::checker::Ty;
use bynk_check::locals::{LocalBinding, locals_at};
use bynk_check::requirements::Requirement;
use bynk_syntax::ast::*;
use bynk_syntax::lexer::tokenize;
use bynk_syntax::parser::parse_unit_with_recovery;
use bynk_syntax::span::Span;
use std::collections::HashMap;
use tower_lsp::lsp_types::*;
pub fn extract_variable(
text: &str,
requested: Span,
uri: &Url,
version: Option<i32>,
) -> Vec<CodeActionOrCommand> {
if requested.start == requested.end {
return Vec::new();
}
let Ok(tokens) = tokenize(text) else {
return Vec::new();
};
let (Some(unit), _errs) = parse_unit_with_recovery(&tokens, text) else {
return Vec::new();
};
let Some(site) = find_site(&unit, requested) else {
return Vec::new();
};
let selected = &text[site.expr_span.start..site.expr_span.end];
let name = fresh_name(text);
let indent = line_indent(text, site.insertion_offset);
let insert_pos = crate::position::offset_to_position(text, site.insertion_offset);
let edits = vec![
OneOf::Left(TextEdit {
range: Range::new(insert_pos, insert_pos),
new_text: format!("let {name} = {selected}\n{indent}"),
}),
OneOf::Left(TextEdit {
range: crate::position::span_to_range(text, site.expr_span),
new_text: name.clone(),
}),
];
vec![CodeActionOrCommand::CodeAction(CodeAction {
title: format!("Extract variable `{name}`"),
kind: Some(CodeActionKind::REFACTOR_EXTRACT),
edit: Some(WorkspaceEdit {
changes: None,
document_changes: Some(DocumentChanges::Edits(vec![TextDocumentEdit {
text_document: OptionalVersionedTextDocumentIdentifier {
uri: uri.clone(),
version,
},
edits,
}])),
change_annotations: None,
}),
..Default::default()
})]
}
pub fn extract_function(
text: &str,
requested: Span,
uri: &Url,
version: Option<i32>,
requirements: &[Requirement],
locals: &[LocalBinding],
expr_types: &[(Span, Ty)],
) -> Vec<CodeActionOrCommand> {
if requested.start == requested.end {
return Vec::new();
}
let Ok(tokens) = tokenize(text) else {
return Vec::new();
};
let (Some(unit), _errs) = parse_unit_with_recovery(&tokens, text) else {
return Vec::new();
};
let Some(site) = find_function_site(&unit, requested, text) else {
return Vec::new();
};
if requirements.iter().any(|r| contains(site.span, r.site)) {
return Vec::new();
}
let mut exprs: Vec<&Expr> = Vec::new();
let (ret_ty_display, call_site_form): (String, CallSiteForm) = match &site.selection {
FunctionSelection::Expr(expr) => {
let Some(ret_ty) = ty_at_span(expr_types, site.span) else {
return Vec::new();
};
exprs.push(expr);
(ret_ty.display(), CallSiteForm::Bare)
}
FunctionSelection::Stmts {
stmts,
tail,
remainder,
} => {
if stmts_contain_assign_stmt(stmts) {
return Vec::new();
}
for s in *stmts {
statement_exprs(s, &mut exprs);
}
match tail {
Some(t) => {
let Some(ret_ty) = ty_at_span(expr_types, t.span) else {
return Vec::new();
};
exprs.push(t);
(ret_ty.display(), CallSiteForm::Bare)
}
None => {
if leaks_a_binding(stmts, remainder, locals, site.span) {
return Vec::new();
}
if stmts_contain_effect_stmt(stmts) {
("Effect[()]".to_string(), CallSiteForm::Do)
} else {
("()".to_string(), CallSiteForm::Discard)
}
}
}
}
};
let mut idents = Vec::new();
for e in &exprs {
collect_idents(e, &mut idents);
}
let mut free: Vec<(&str, Span, &str)> = Vec::new();
for id in &idents {
let Some(binding) = locals_at(locals, id.span.start)
.into_iter()
.find(|b| b.name == id.name)
else {
continue;
};
if contains(site.span, binding.def_span) {
continue; }
free.push((id.name.as_str(), binding.def_span, binding.ty.as_str()));
}
let mut def_by_name: HashMap<&str, Span> = HashMap::new();
for (name, def_span, _) in &free {
match def_by_name.get(name) {
Some(prev) if *prev != *def_span => return Vec::new(),
_ => {
def_by_name.insert(name, *def_span);
}
}
}
let mut params: Vec<(&str, &str)> = Vec::new();
for (name, _, ty) in &free {
if !params.iter().any(|(n, _)| n == name) {
params.push((name, ty));
}
}
let fn_name = fresh_word(text, "extractedFn");
let param_list = params
.iter()
.map(|(n, t)| format!("{n}: {t}"))
.collect::<Vec<_>>()
.join(", ");
let arg_list = params
.iter()
.map(|(n, _)| n.to_string())
.collect::<Vec<_>>()
.join(", ");
let selected = &text[site.span.start..site.span.end];
let new_fn = format!("fn {fn_name}({param_list}) -> {ret_ty_display} {{\n {selected}\n}}\n\n");
let insert_pos = crate::position::offset_to_position(text, site.item_start);
let call_expr = format!("{fn_name}({arg_list})");
let call_site_text = match call_site_form {
CallSiteForm::Bare => call_expr,
CallSiteForm::Discard => format!("let _ = {call_expr}"),
CallSiteForm::Do => format!("do {call_expr}"),
};
let edits = vec![
OneOf::Left(TextEdit {
range: Range::new(insert_pos, insert_pos),
new_text: new_fn,
}),
OneOf::Left(TextEdit {
range: crate::position::span_to_range(text, site.span),
new_text: call_site_text,
}),
];
vec![CodeActionOrCommand::CodeAction(CodeAction {
title: format!("Extract function `{fn_name}`"),
kind: Some(CodeActionKind::REFACTOR_EXTRACT),
edit: Some(WorkspaceEdit {
changes: None,
document_changes: Some(DocumentChanges::Edits(vec![TextDocumentEdit {
text_document: OptionalVersionedTextDocumentIdentifier {
uri: uri.clone(),
version,
},
edits,
}])),
change_annotations: None,
}),
..Default::default()
})]
}
struct Site<'a> {
insertion_offset: usize,
expr_span: Span,
expr: &'a Expr,
}
fn contains(outer: Span, inner: Span) -> bool {
outer.start <= inner.start && inner.end <= outer.end
}
fn find_site(unit: &SourceUnit, target: Span) -> Option<Site<'_>> {
match unit {
SourceUnit::Commons(c) => find_in_items(&c.items, target),
SourceUnit::Context(c) => find_in_items(&c.items, target),
SourceUnit::Adapter(_) => None,
SourceUnit::Suite(t) => t
.cases
.iter()
.find(|case| contains(case.body.span, target))
.and_then(|case| find_in_block(&case.body, target)),
}
}
fn find_in_items(items: &[CommonsItem], target: Span) -> Option<Site<'_>> {
for item in items {
match item {
CommonsItem::Fn(f) if contains(f.body.span, target) => {
return find_in_block(&f.body, target);
}
CommonsItem::Provider(p) => {
if let Some(op) = p.ops.iter().find(|op| contains(op.body.span, target)) {
return find_in_block(&op.body, target);
}
}
CommonsItem::Service(s) => {
if let Some(h) = s.handlers.iter().find(|h| contains(h.body.span, target)) {
return find_in_block(&h.body, target);
}
}
CommonsItem::Agent(a) => {
if let Some(h) = a.handlers.iter().find(|h| contains(h.body.span, target)) {
return find_in_block(&h.body, target);
}
}
_ => {}
}
}
None
}
fn find_in_block(block: &Block, target: Span) -> Option<Site<'_>> {
for stmt in &block.statements {
let mut values = Vec::new();
statement_exprs(stmt, &mut values);
let Some(value) = values.first() else {
continue;
};
if contains(value.span, target) {
return Some(locate(value, target, stmt.span().start));
}
}
if contains(block.tail.span, target) {
return Some(locate(&block.tail, target, block.tail.span.start));
}
None
}
fn locate(expr: &Expr, target: Span, insertion_offset: usize) -> Site<'_> {
match &expr.kind {
ExprKind::Block(b) => find_in_block(b, target).unwrap_or(Site {
insertion_offset,
expr_span: expr.span,
expr,
}),
ExprKind::If {
cond,
then_block,
else_block,
} => {
if contains(cond.span, target) {
return locate(cond, target, insertion_offset);
}
if contains(then_block.span, target)
&& let Some(site) = find_in_block(then_block, target)
{
return site;
}
if contains(else_block.span, target)
&& let Some(site) = find_in_block(else_block, target)
{
return site;
}
Site {
insertion_offset,
expr_span: expr.span,
expr,
}
}
ExprKind::Match { discriminant, arms } => {
if contains(discriminant.span, target) {
return locate(discriminant, target, insertion_offset);
}
for arm in arms {
if !contains(arm.body.span(), target) {
continue;
}
return match &arm.body {
MatchBody::Expr(e) => locate(e, target, insertion_offset),
MatchBody::Block(b) => find_in_block(b, target).unwrap_or(Site {
insertion_offset,
expr_span: expr.span,
expr,
}),
};
}
Site {
insertion_offset,
expr_span: expr.span,
expr,
}
}
_ => {
let children = expr_children(expr);
match children.into_iter().find(|c| contains(c.span, target)) {
Some(child) => locate(child, target, insertion_offset),
None => Site {
insertion_offset,
expr_span: expr.span,
expr,
},
}
}
}
}
fn fresh_name(text: &str) -> String {
fresh_word(text, "extracted")
}
fn fresh_word(text: &str, base: &str) -> String {
let is_ident_char = |c: char| c.is_alphanumeric() || c == '_';
let occurs_as_word = |candidate: &str| {
text.match_indices(candidate).any(|(i, _)| {
let before_ok = text[..i]
.chars()
.next_back()
.is_none_or(|c| !is_ident_char(c));
let after_ok = text[i + candidate.len()..]
.chars()
.next()
.is_none_or(|c| !is_ident_char(c));
before_ok && after_ok
})
};
let mut n = 1;
loop {
let candidate = if n == 1 {
base.to_string()
} else {
format!("{base}{n}")
};
if !occurs_as_word(&candidate) {
return candidate;
}
n += 1;
}
}
fn item_span(item: &CommonsItem) -> Option<Span> {
match item {
CommonsItem::Fn(f) => Some(f.span),
CommonsItem::Provider(p) => Some(p.span),
CommonsItem::Service(s) => Some(s.span),
CommonsItem::Agent(a) => Some(a.span),
_ => None,
}
}
enum FunctionSelection<'a> {
Expr(&'a Expr),
Stmts {
stmts: &'a [Statement],
tail: Option<&'a Expr>,
remainder: Vec<&'a Expr>,
},
}
enum CallSiteForm {
Bare,
Discard,
Do,
}
struct FunctionSite<'a> {
item_start: usize,
span: Span,
selection: FunctionSelection<'a>,
}
fn find_function_site<'a>(
unit: &'a SourceUnit,
target: Span,
text: &str,
) -> Option<FunctionSite<'a>> {
let items = match unit {
SourceUnit::Commons(c) => &c.items,
SourceUnit::Context(c) => &c.items,
SourceUnit::Adapter(_) | SourceUnit::Suite(_) => return None,
};
let item = items
.iter()
.find(|it| item_span(it).is_some_and(|s| contains(s, target)))?;
let item_start = item_span(item)?.start;
let trimmed = trim_span(text, target);
if trimmed.start < trimmed.end
&& let Some((stmts, tail, remainder)) = find_multi_stmt_in_item(item, trimmed)
{
return Some(FunctionSite {
item_start,
span: trimmed,
selection: FunctionSelection::Stmts {
stmts,
tail,
remainder,
},
});
}
let site = find_in_items(std::slice::from_ref(item), target)?;
Some(FunctionSite {
item_start,
span: site.expr_span,
selection: FunctionSelection::Expr(site.expr),
})
}
fn trim_span(text: &str, span: Span) -> Span {
let s = &text[span.start..span.end];
let start = span.start + (s.len() - s.trim_start().len());
let trimmed_len = s.trim().len();
Span::new(start, start + trimmed_len)
}
type StmtRun<'a> = (&'a [Statement], Option<&'a Expr>, Vec<&'a Expr>);
fn find_multi_stmt_in_item(item: &CommonsItem, target: Span) -> Option<StmtRun<'_>> {
match item {
CommonsItem::Fn(f) if contains(f.body.span, target) => find_stmt_run(&f.body, target),
CommonsItem::Provider(p) => p
.ops
.iter()
.find(|op| contains(op.body.span, target))
.and_then(|op| find_stmt_run(&op.body, target)),
CommonsItem::Service(s) => s
.handlers
.iter()
.find(|h| contains(h.body.span, target))
.and_then(|h| find_stmt_run(&h.body, target)),
CommonsItem::Agent(a) => a
.handlers
.iter()
.find(|h| contains(h.body.span, target))
.and_then(|h| find_stmt_run(&h.body, target)),
_ => None,
}
}
fn find_stmt_run(block: &Block, target: Span) -> Option<StmtRun<'_>> {
for stmt in &block.statements {
let mut values = Vec::new();
statement_exprs(stmt, &mut values);
for v in values {
if let Some(found) = find_stmt_run_in_expr(v, target) {
return Some(found);
}
}
}
if let Some(found) = find_stmt_run_in_expr(&block.tail, target) {
return Some(found);
}
align_stmt_run(block, target)
}
fn find_stmt_run_in_expr(e: &Expr, target: Span) -> Option<StmtRun<'_>> {
if !contains(e.span, target) {
return None;
}
match &e.kind {
ExprKind::Block(b) => find_stmt_run(b, target),
ExprKind::If {
cond,
then_block,
else_block,
} => find_stmt_run_in_expr(cond, target)
.or_else(|| find_stmt_run(then_block, target))
.or_else(|| find_stmt_run(else_block, target)),
ExprKind::Match { discriminant, arms } => find_stmt_run_in_expr(discriminant, target)
.or_else(|| {
arms.iter().find_map(|arm| match &arm.body {
MatchBody::Expr(e) => find_stmt_run_in_expr(e, target),
MatchBody::Block(b) => find_stmt_run(b, target),
})
}),
_ => expr_children(e)
.into_iter()
.find_map(|c| find_stmt_run_in_expr(c, target)),
}
}
fn align_stmt_run(block: &Block, target: Span) -> Option<StmtRun<'_>> {
let start_idx = block
.statements
.iter()
.position(|s| s.span().start == target.start)?;
if target.end == block.tail.span.end {
let full = Span::new(
block.statements[start_idx].span().start,
block.tail.span.end,
);
if full == target {
return Some((
&block.statements[start_idx..],
Some(block.tail.as_ref()),
Vec::new(),
));
}
}
let end_idx = block
.statements
.iter()
.position(|s| s.span().end == target.end)?;
if end_idx < start_idx {
return None;
}
let full = Span::new(
block.statements[start_idx].span().start,
block.statements[end_idx].span().end,
);
if full != target {
return None;
}
let mut remainder = Vec::new();
for s in &block.statements[end_idx + 1..] {
statement_exprs(s, &mut remainder);
}
remainder.push(&block.tail);
Some((&block.statements[start_idx..=end_idx], None, remainder))
}
fn leaks_a_binding(
stmts: &[Statement],
remainder: &[&Expr],
locals: &[LocalBinding],
run_span: Span,
) -> bool {
let bound_names: Vec<&str> = stmts
.iter()
.filter_map(|s| match s {
Statement::Let(l) | Statement::EffectLet(l) if l.name.name != "_" => {
Some(l.name.name.as_str())
}
_ => None,
})
.collect();
if bound_names.is_empty() {
return false;
}
let mut used = Vec::new();
for e in remainder {
collect_idents(e, &mut used);
}
used.iter().any(|id| {
bound_names.contains(&id.name.as_str())
&& locals_at(locals, id.span.start)
.into_iter()
.find(|b| b.name == id.name)
.is_some_and(|b| contains(run_span, b.def_span))
})
}
fn stmts_match(stmts: &[Statement], pred: &impl Fn(&Statement) -> bool) -> bool {
stmts.iter().any(|s| pred(s) || stmt_value_matches(s, pred))
}
fn stmt_value_matches(s: &Statement, pred: &impl Fn(&Statement) -> bool) -> bool {
match s {
Statement::Let(l) | Statement::EffectLet(l) => expr_matches(&l.value, pred),
Statement::Expect(a) => expr_matches(&a.value, pred),
Statement::Send(snd) => expr_matches(&snd.value, pred),
Statement::Do(d) => expr_matches(&d.value, pred),
Statement::Assign(a) => expr_matches(&a.value, pred),
}
}
fn block_matches(b: &Block, pred: &impl Fn(&Statement) -> bool) -> bool {
stmts_match(&b.statements, pred) || expr_matches(&b.tail, pred)
}
fn expr_matches(e: &Expr, pred: &impl Fn(&Statement) -> bool) -> bool {
match &e.kind {
ExprKind::Block(b) => block_matches(b, pred),
ExprKind::If {
cond,
then_block,
else_block,
} => {
expr_matches(cond, pred)
|| block_matches(then_block, pred)
|| block_matches(else_block, pred)
}
ExprKind::Match { discriminant, arms } => {
expr_matches(discriminant, pred)
|| arms.iter().any(|arm| match &arm.body {
MatchBody::Expr(e) => expr_matches(e, pred),
MatchBody::Block(b) => block_matches(b, pred),
})
}
_ => expr_children(e).into_iter().any(|c| expr_matches(c, pred)),
}
}
fn stmts_contain_effect_stmt(stmts: &[Statement]) -> bool {
stmts_match(stmts, &|s| {
matches!(
s,
Statement::EffectLet(_) | Statement::Send(_) | Statement::Do(_)
)
})
}
fn stmts_contain_assign_stmt(stmts: &[Statement]) -> bool {
stmts_match(stmts, &|s| matches!(s, Statement::Assign(_)))
}
fn collect_idents<'a>(expr: &'a Expr, out: &mut Vec<&'a Ident>) {
if let ExprKind::Ident(id) = &expr.kind {
out.push(id);
}
for child in expr_children(expr) {
collect_idents(child, out);
}
}
fn ty_at_span(entries: &[(Span, Ty)], span: Span) -> Option<&Ty> {
entries.iter().find(|(s, _)| *s == span).map(|(_, t)| t)
}
fn line_indent(text: &str, offset: usize) -> String {
let line_start = text[..offset].rfind('\n').map_or(0, |i| i + 1);
let prefix = &text[line_start..offset];
if prefix.chars().all(|c| c == ' ' || c == '\t') {
prefix.to_string()
} else {
String::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn actions_for(text: &str, needle: &str) -> Vec<CodeActionOrCommand> {
let start = text.find(needle).expect("needle present");
let requested = Span::new(start, start + needle.len());
let uri = Url::parse("file:///a.bynk").unwrap();
extract_variable(text, requested, &uri, Some(3))
}
fn sole_edit(action: &CodeActionOrCommand) -> Vec<TextEdit> {
let CodeActionOrCommand::CodeAction(a) = action else {
panic!("expected a CodeAction");
};
assert_eq!(a.kind, Some(CodeActionKind::REFACTOR_EXTRACT));
let Some(DocumentChanges::Edits(doc_edits)) = &a.edit.as_ref().unwrap().document_changes
else {
panic!("expected versioned document edits");
};
assert_eq!(doc_edits[0].text_document.version, Some(3));
doc_edits[0]
.edits
.iter()
.map(|e| {
let OneOf::Left(edit) = e else {
panic!("expected a plain TextEdit")
};
edit.clone()
})
.collect()
}
#[test]
fn extracts_a_sub_expression_of_a_let_value() {
let src = "context c\n\nfn f() -> Int {\n let y = 1 + 2\n y\n}\n";
let actions = actions_for(src, "1 + 2");
assert_eq!(actions.len(), 1);
let edits = sole_edit(&actions[0]);
assert_eq!(edits.len(), 2);
assert!(edits[0].new_text.starts_with("let extracted = 1 + 2\n"));
assert_eq!(edits[1].new_text, "extracted");
assert_eq!(edits[0].range.start, edits[0].range.end);
assert_eq!(edits[0].range.start.line, edits[1].range.start.line);
assert!(edits[0].range.start.character < edits[1].range.start.character);
}
#[test]
fn extracts_the_whole_tail_expression() {
let src = "context c\n\nfn f() -> Int {\n let x = 1\n x + 1\n}\n";
let actions = actions_for(src, "x + 1");
let edits = sole_edit(&actions[0]);
assert!(edits[0].new_text.starts_with("let extracted = x + 1\n"));
assert_eq!(edits[1].new_text, "extracted");
}
#[test]
fn resets_the_insertion_point_inside_a_nested_if_block() {
let src = concat!(
"context c\n\n",
"fn f(n: Int) -> Int {\n",
" if n > 0 {\n",
" let z = n * 2\n",
" z\n",
" } else {\n",
" 0\n",
" }\n",
"}\n",
);
let actions = actions_for(src, "n * 2");
let edits = sole_edit(&actions[0]);
let insert_line = edits[0].range.start.line;
let let_z_line = crate::position::offset_to_position(src, src.find("let z").unwrap()).line;
assert_eq!(insert_line, let_z_line);
}
#[test]
fn resets_the_insertion_point_inside_a_nested_match_arm_block() {
let src = concat!(
"context c\n\n",
"fn f(n: Int) -> Int {\n",
" match n {\n",
" 0 => {\n",
" let z = n * 2\n",
" z\n",
" }\n",
" _ => 0\n",
" }\n",
"}\n",
);
let actions = actions_for(src, "n * 2");
let edits = sole_edit(&actions[0]);
let insert_line = edits[0].range.start.line;
let let_z_line = crate::position::offset_to_position(src, src.find("let z").unwrap()).line;
assert_eq!(insert_line, let_z_line);
}
#[test]
fn selection_crossing_a_statement_boundary_offers_nothing() {
let src = "context c\n\nfn f() -> Int {\n let a = 1\n let b = 2\n a\n}\n";
let actions = actions_for(src, "1\n let b");
assert!(actions.is_empty());
}
#[test]
fn selection_of_the_whole_block_braces_included_offers_nothing() {
let src = "context c\n\nfn f() -> Int {\n let a = 1\n a\n}\n";
let actions = actions_for(src, "{\n let a = 1\n a\n}");
assert!(actions.is_empty());
}
#[test]
fn extracts_a_call_argument() {
let src = "context c\n\nfn f() -> Int {\n g(1 + 2, 3)\n}\n";
let actions = actions_for(src, "1 + 2");
let edits = sole_edit(&actions[0]);
assert_eq!(edits[1].new_text, "extracted");
assert!(edits[0].new_text.contains("let extracted = 1 + 2"));
}
#[test]
fn bumps_the_suffix_on_a_name_collision() {
let src = "context c\n\nfn f() -> Int {\n let extracted = 0\n 1 + 2\n}\n";
let actions = actions_for(src, "1 + 2");
let edits = sole_edit(&actions[0]);
assert!(edits[0].new_text.starts_with("let extracted2 = 1 + 2\n"));
assert_eq!(edits[1].new_text, "extracted2");
}
#[test]
fn empty_selection_offers_nothing() {
let src = "context c\n\nfn f() -> Int {\n 1 + 2\n}\n";
let off = src.find("1 + 2").unwrap();
let uri = Url::parse("file:///a.bynk").unwrap();
let actions = extract_variable(src, Span::new(off, off), &uri, Some(1));
assert!(actions.is_empty());
}
#[test]
fn selection_outside_any_body_offers_nothing() {
let src = "context c\n\nfn f() -> Int {\n 1 + 2\n}\n";
let actions = actions_for(src, "context c");
assert!(actions.is_empty());
}
#[test]
fn preserves_indentation_on_a_multi_statement_block() {
let src = "context c\n\nfn f() -> Int {\n let a = 1\n let b = a + 2\n b\n}\n";
let actions = actions_for(src, "a + 2");
let edits = sole_edit(&actions[0]);
assert_eq!(edits[0].new_text, "let extracted = a + 2\n ");
}
mod extract_function_tests {
use super::*;
use bynk_check::locals::LocalKind;
use bynk_check::requirements::RequirementSource;
fn function_actions_for(
text: &str,
needle: &str,
requirements: &[Requirement],
locals: &[LocalBinding],
expr_types: &[(Span, Ty)],
) -> Vec<CodeActionOrCommand> {
let start = text.find(needle).expect("needle present");
let requested = Span::new(start, start + needle.len());
let uri = Url::parse("file:///a.bynk").unwrap();
extract_function(
text,
requested,
&uri,
Some(3),
requirements,
locals,
expr_types,
)
}
fn nth_offset(text: &str, needle: &str, n: usize) -> usize {
text.match_indices(needle)
.nth(n)
.expect("occurrence present")
.0
}
fn param(text: &str, name: &str, ty: &str) -> LocalBinding {
let def_start = nth_offset(text, name, 0);
LocalBinding {
name: name.to_string(),
def_span: Span::new(def_start, def_start + name.len()),
kind: LocalKind::Param,
ty: ty.to_string(),
scope: Span::new(0, text.len()),
}
}
fn int_type(text: &str, needle: &str) -> (Span, Ty) {
let start = text.find(needle).expect("needle present");
(
Span::new(start, start + needle.len()),
Ty::Base(BaseType::Int),
)
}
fn let_binding(text: &str, name: &str, ty: &str) -> LocalBinding {
let def_start = nth_offset(text, name, 0);
LocalBinding {
name: name.to_string(),
def_span: Span::new(def_start, def_start + name.len()),
kind: LocalKind::Let,
ty: ty.to_string(),
scope: Span::new(0, text.len()),
}
}
fn capability_use(site_needle_offset: usize, len: usize) -> Requirement {
Requirement {
capability: "Clock".to_string(),
site: Span::new(site_needle_offset, site_needle_offset + len),
source: RequirementSource::DirectCall {
op: "now".to_string(),
},
covered: false,
materialize: None,
}
}
#[test]
fn extracts_a_free_variable_as_a_parameter() {
let src = "context c\n\nfn f(num: Int) -> Int {\n num * 2\n}\n";
let locals = vec![param(src, "num", "Int")];
let types = vec![int_type(src, "num * 2")];
let actions = function_actions_for(src, "num * 2", &[], &locals, &types);
assert_eq!(actions.len(), 1);
let edits = sole_edit(&actions[0]);
assert_eq!(
edits[0].new_text,
"fn extractedFn(num: Int) -> Int {\n num * 2\n}\n\n"
);
assert_eq!(edits[0].range.start, Position::new(2, 0));
assert_eq!(edits[1].new_text, "extractedFn(num)");
}
#[test]
fn no_free_variables_yields_a_nullary_call() {
let src = "context c\n\nfn f() -> Int {\n 1 + 2\n}\n";
let types = vec![int_type(src, "1 + 2")];
let actions = function_actions_for(src, "1 + 2", &[], &[], &types);
let edits = sole_edit(&actions[0]);
assert_eq!(
edits[0].new_text,
"fn extractedFn() -> Int {\n 1 + 2\n}\n\n"
);
assert_eq!(edits[1].new_text, "extractedFn()");
}
#[test]
fn capability_using_selection_offers_nothing() {
let src = "context c\n\nfn f() -> Int {\n 1 + 2\n}\n";
let site = src.find("1 + 2").unwrap();
let reqs = vec![capability_use(site, "1 + 2".len())];
let types = vec![int_type(src, "1 + 2")];
let actions = function_actions_for(src, "1 + 2", &reqs, &[], &types);
assert!(actions.is_empty());
}
#[test]
fn bumps_the_suffix_on_a_name_collision() {
let src =
"context c\n\nfn extractedFn() -> Int {\n 0\n}\n\nfn f() -> Int {\n 1 + 2\n}\n";
let types = vec![int_type(src, "1 + 2")];
let actions = function_actions_for(src, "1 + 2", &[], &[], &types);
let edits = sole_edit(&actions[0]);
assert!(edits[0].new_text.starts_with("fn extractedFn2("));
assert_eq!(edits[1].new_text, "extractedFn2()");
}
#[test]
fn selection_outside_any_body_offers_nothing() {
let src = "context c\n\nfn f() -> Int {\n 1 + 2\n}\n";
let types = vec![int_type(src, "1 + 2")];
let actions = function_actions_for(src, "context c", &[], &[], &types);
assert!(actions.is_empty());
}
#[test]
fn adapter_files_offer_nothing() {
let src = "adapter tokens {\n binding \"./b.ts\"\n}\n";
let actions = function_actions_for(src, "binding", &[], &[], &[]);
assert!(actions.is_empty());
}
#[test]
fn suite_test_cases_offer_nothing() {
let src = "suite thing\n\ncase \"it works\" {\n 1 + 2\n}\n";
let types = vec![int_type(src, "1 + 2")];
let actions = function_actions_for(src, "1 + 2", &[], &[], &types);
assert!(actions.is_empty());
}
#[test]
fn a_dirty_file_with_no_recorded_type_offers_nothing() {
let src = "context c\n\nfn f() -> Int {\n 1 + 2\n}\n";
let actions = function_actions_for(src, "1 + 2", &[], &[], &[]);
assert!(actions.is_empty());
}
#[test]
fn ambiguous_same_name_shadow_from_two_outer_scopes_declines() {
let src = "context c\n\nfn f() -> Int {\n x + x\n}\n";
let types = vec![int_type(src, "x + x")];
let first_x = nth_offset(src, "x + x", 0); let second_x = first_x + "x + ".len(); let locals = vec![
LocalBinding {
name: "x".to_string(),
def_span: Span::new(0, 1),
kind: LocalKind::Let,
ty: "Int".to_string(),
scope: Span::new(first_x, first_x),
},
LocalBinding {
name: "x".to_string(),
def_span: Span::new(1, 2),
kind: LocalKind::Let,
ty: "Int".to_string(),
scope: Span::new(second_x, second_x),
},
];
let actions = function_actions_for(src, "x + x", &[], &locals, &types);
assert!(actions.is_empty());
}
mod multi_statement_tests {
use super::*;
#[test]
fn a_run_including_the_tail_becomes_a_call_expression() {
let src = "context c\n\nfn f(num: Int) -> Int {\n let a = num + 1\n let b = a * 2\n b + num\n}\n";
let locals = vec![
param(src, "num", "Int"),
let_binding(src, "a", "Int"),
let_binding(src, "b", "Int"),
];
let types = vec![int_type(src, "b + num")];
let needle = "let a = num + 1\n let b = a * 2\n b + num";
let actions = function_actions_for(src, needle, &[], &locals, &types);
assert_eq!(actions.len(), 1);
let edits = sole_edit(&actions[0]);
assert_eq!(
edits[0].new_text,
format!("fn extractedFn(num: Int) -> Int {{\n {needle}\n}}\n\n")
);
assert_eq!(edits[1].new_text, "extractedFn(num)");
}
#[test]
fn a_run_excluding_the_tail_yields_a_discard_call() {
let src = "context c\n\nfn f(num: Int) -> Int {\n let a = num + 1\n let valC = a * 2\n num\n}\n";
let locals = vec![
param(src, "num", "Int"),
let_binding(src, "a", "Int"),
let_binding(src, "valC", "Int"),
];
let needle = "let a = num + 1\n let valC = a * 2";
let actions = function_actions_for(src, needle, &[], &locals, &[]);
assert_eq!(actions.len(), 1);
let edits = sole_edit(&actions[0]);
assert_eq!(
edits[0].new_text,
format!("fn extractedFn(num: Int) -> () {{\n {needle}\n}}\n\n")
);
assert_eq!(edits[1].new_text, "let _ = extractedFn(num)");
}
#[test]
fn an_effectful_run_excluding_the_tail_yields_an_effect_return_and_a_do_call() {
let src = "context c\n\nfn f() -> Effect[()] {\n do g()\n do h()\n ()\n}\n";
let needle = "do g()\n do h()";
let actions = function_actions_for(src, needle, &[], &[], &[]);
assert_eq!(actions.len(), 1);
let edits = sole_edit(&actions[0]);
assert_eq!(
edits[0].new_text,
format!("fn extractedFn() -> Effect[()] {{\n {needle}\n}}\n\n")
);
assert_eq!(edits[1].new_text, "do extractedFn()");
}
#[test]
fn a_binding_the_run_introduces_still_used_in_the_tail_declines() {
let src = "context c\n\nfn f(num: Int) -> Int {\n let a = num + 1\n let b = a * 2\n b\n}\n";
let locals = vec![
param(src, "num", "Int"),
let_binding(src, "a", "Int"),
let_binding(src, "b", "Int"),
];
let needle = "let a = num + 1\n let b = a * 2";
let actions = function_actions_for(src, needle, &[], &locals, &[]);
assert!(actions.is_empty());
}
#[test]
fn a_run_containing_a_cell_write_declines() {
let src = "context c\n\nfn f() -> Int {\n cell := 1\n x\n}\n";
let actions = function_actions_for(src, "cell := 1", &[], &[], &[]);
assert!(actions.is_empty());
}
#[test]
fn a_run_containing_a_nested_cell_write_declines() {
let src = concat!(
"context c\n\n",
"fn f(cond: Bool) -> Int {\n",
" let a = if cond {\n",
" cell := 1\n",
" 0\n",
" } else {\n",
" 1\n",
" }\n",
" a\n",
"}\n",
);
let needle = "let a = if cond {\n cell := 1\n 0\n } else {\n 1\n }";
let actions = function_actions_for(src, needle, &[], &[], &[]);
assert!(actions.is_empty());
}
#[test]
fn a_run_found_inside_a_nested_if_branch() {
let src = concat!(
"context c\n\n",
"fn f(num: Int) -> Int {\n",
" if num > 0 {\n",
" let valA = num * 2\n",
" let valB = valA + 1\n",
" valB\n",
" } else {\n",
" 0\n",
" }\n",
"}\n",
);
let locals = vec![
param(src, "num", "Int"),
let_binding(src, "valA", "Int"),
let_binding(src, "valB", "Int"),
];
let tail_val_b = nth_offset(src, "valB", 1); let types = vec![(
Span::new(tail_val_b, tail_val_b + "valB".len()),
Ty::Base(BaseType::Int),
)];
let needle = "let valA = num * 2\n let valB = valA + 1\n valB";
let actions = function_actions_for(src, needle, &[], &locals, &types);
assert_eq!(actions.len(), 1);
let edits = sole_edit(&actions[0]);
assert_eq!(
edits[0].new_text,
format!("fn extractedFn(num: Int) -> Int {{\n {needle}\n}}\n\n")
);
assert_eq!(edits[0].range.start, Position::new(2, 0));
assert_eq!(edits[1].new_text, "extractedFn(num)");
}
#[test]
fn a_selection_extending_past_a_statement_boundary_declines() {
let src = "context c\n\nfn f(num: Int) -> Int {\n let a = num + 1\n let valC = a * 2\n num\n}\n";
let start = src.find("let a = num + 1").unwrap();
let end = start + "let a = num + 1\n let valC = a * 2".len() - 1;
let requested = Span::new(start, end);
let uri = Url::parse("file:///a.bynk").unwrap();
let actions = extract_function(src, requested, &uri, Some(3), &[], &[], &[]);
assert!(actions.is_empty());
}
#[test]
fn a_selection_padded_with_surrounding_whitespace_still_aligns() {
let src = "context c\n\nfn f(num: Int) -> Int {\n let a = num + 1\n let valC = a * 2\n num\n}\n";
let inner = "let a = num + 1\n let valC = a * 2";
let start = src.find(inner).unwrap();
let requested = Span::new(start - 2, start + inner.len() + 1);
let locals = vec![
param(src, "num", "Int"),
let_binding(src, "a", "Int"),
let_binding(src, "valC", "Int"),
];
let uri = Url::parse("file:///a.bynk").unwrap();
let actions = extract_function(src, requested, &uri, Some(3), &[], &locals, &[]);
assert_eq!(actions.len(), 1);
let edits = sole_edit(&actions[0]);
assert_eq!(edits[1].new_text, "let _ = extractedFn(num)");
}
}
}
}