use std::path::PathBuf;
use blends_domain::query::{adj_ast, get_all_scope_definitions, get_args_n_ids, matching_nodes};
use blends_domain::syntax::{SyntaxGraph, SyntaxKind, SyntaxNode};
use blends_domain::NodeId;
use crate::{get_ast_graph, get_syntax_graph, Content, Language};
fn dedent(code: &str) -> String {
let lines: Vec<&str> = code.trim_matches('\n').lines().collect();
let indent = lines
.iter()
.filter(|line| !line.trim().is_empty())
.filter_map(|line| line.find(|c: char| !c.is_whitespace()))
.min()
.unwrap_or(0);
lines
.iter()
.map(|line| line.get(indent..).unwrap_or(""))
.collect::<Vec<_>>()
.join("\n")
}
fn syntax_graph(code: &str, language: Language) -> SyntaxGraph {
let snippet = dedent(code);
let content = Content {
bytes: snippet.as_bytes().to_vec(),
text: snippet,
language,
path: PathBuf::from("test_file"),
};
let ast = get_ast_graph(&content).expect("snippet should parse");
get_syntax_graph(&ast, &content, None, None).expect("syntax graph should be built")
}
fn node(graph: &SyntaxGraph, n_id: NodeId) -> &SyntaxNode {
graph.nodes.get(&n_id).expect("node should exist")
}
fn single(n_ids: &[NodeId]) -> NodeId {
let &[n_id] = n_ids else {
panic!("expected exactly one node, got {n_ids:?}");
};
n_id
}
fn invocation(graph: &SyntaxGraph, expression: &str) -> NodeId {
matching_nodes(graph, SyntaxKind::MethodInvocation)
.into_iter()
.find(|n_id| node(graph, *n_id).expression() == Some(expression))
.expect("invocation should exist")
}
fn sink_argument(graph: &SyntaxGraph) -> NodeId {
single(&get_args_n_ids(graph, invocation(graph, "sink")))
}
fn read_inside(graph: &SyntaxGraph, root_id: NodeId, symbol: &str) -> NodeId {
let reads: Vec<NodeId> = adj_ast(graph, root_id, Some(-1), &["SymbolLookup"])
.into_iter()
.filter(|n_id| node(graph, *n_id).symbol() == Some(symbol))
.collect();
single(&reads)
}
fn definition_values(graph: &SyntaxGraph, lookup_id: NodeId) -> Vec<&str> {
get_all_scope_definitions(graph, lookup_id)
.into_iter()
.map(|def_id| {
let value_id = node(graph, def_id)
.value_id()
.expect("definition should have a value");
node(graph, value_id).expression().unwrap_or_default()
})
.collect()
}
#[test]
fn self_assignment_read_resolves_to_previous_definition() {
let graph = syntax_graph(
r"
function f() {
let x = source()
if (x) {
x = x.trim()
}
}
",
Language::TypeScript,
);
let inner_read = read_inside(&graph, invocation(&graph, "x.trim"), "x");
assert_eq!(definition_values(&graph, inner_read), ["source"]);
}
#[test]
fn self_assignment_read_in_same_scope() {
let graph = syntax_graph(
r"
function f() {
let x = source()
x = x.trim()
}
",
Language::JavaScript,
);
let inner_read = read_inside(&graph, invocation(&graph, "x.trim"), "x");
assert!(!definition_values(&graph, inner_read).contains(&"x.trim"));
}
#[test]
fn self_assignment_read_nested_in_arguments() {
let graph = syntax_graph(
r"
function f() {
let x = source()
if (x) {
x = decodeURIComponent(x.trim())
}
}
",
Language::JavaScript,
);
let inner_read = read_inside(&graph, invocation(&graph, "decodeURIComponent"), "x");
assert_eq!(definition_values(&graph, inner_read), ["source"]);
}
#[test]
fn self_assignment_read_python() {
let graph = syntax_graph(
r"
def f():
x = source()
if x:
x = x.strip()
",
Language::Python,
);
let inner_read = read_inside(&graph, invocation(&graph, "x.strip"), "x");
assert_eq!(definition_values(&graph, inner_read), ["source"]);
}
#[test]
fn read_after_self_assignment_resolves_to_it() {
let graph = syntax_graph(
r"
function f() {
let x = source()
if (x) {
x = x.trim()
sink(x)
}
}
",
Language::TypeScript,
);
assert_eq!(definition_values(&graph, sink_argument(&graph)), ["x.trim"]);
}
#[test]
fn read_in_nested_scope_of_value_keeps_definition() {
let graph = syntax_graph(
r"
function f() {
let x = source()
if (x) {
x = wrap(() => x.trim())
}
}
",
Language::JavaScript,
);
let inner_read = read_inside(&graph, invocation(&graph, "wrap"), "x");
assert_eq!(definition_values(&graph, inner_read), ["wrap"]);
}
#[test]
fn definition_in_sibling_branch_is_excluded() {
let graph = syntax_graph(
r"
def f():
if cond():
y = unsafe()
else:
y = safe()
sink(y)
",
Language::Python,
);
assert_eq!(definition_values(&graph, sink_argument(&graph)), ["safe"]);
}
#[test]
fn definition_in_branch_reaches_read_after_if() {
let graph = syntax_graph(
r"
def f():
if cond():
y = unsafe()
else:
y = safe()
sink(y)
",
Language::Python,
);
let mut values = definition_values(&graph, sink_argument(&graph));
values.sort_unstable();
assert_eq!(values, ["safe", "unsafe"]);
}
#[test]
fn definition_in_unrelated_if_is_kept() {
let graph = syntax_graph(
r"
def f():
if cond():
y = unsafe()
if other():
sink(y)
",
Language::Python,
);
assert_eq!(definition_values(&graph, sink_argument(&graph)), ["unsafe"]);
}
#[test]
fn definition_in_nested_if_excluded_from_outer_else() {
let graph = syntax_graph(
r"
def f():
y = safe()
if cond():
if other():
y = unsafe()
else:
sink(y)
",
Language::Python,
);
assert_eq!(definition_values(&graph, sink_argument(&graph)), ["safe"]);
}
#[test]
fn definition_in_elif_sibling_is_excluded() {
let graph = syntax_graph(
r"
def f():
if cond():
y = unsafe()
elif other():
y = safe()
sink(y)
",
Language::Python,
);
assert_eq!(definition_values(&graph, sink_argument(&graph)), ["safe"]);
}
#[test]
fn definition_in_sibling_branch_is_excluded_typescript() {
let graph = syntax_graph(
r"
function f() {
let y = init()
if (cond()) y = unsafe()
else sink(y)
}
",
Language::TypeScript,
);
assert_eq!(definition_values(&graph, sink_argument(&graph)), ["init"]);
}
fn method_reference(graph: &SyntaxGraph) -> NodeId {
let reads: Vec<NodeId> = matching_nodes(graph, SyntaxKind::SymbolLookup)
.into_iter()
.filter(|n_id| node(graph, *n_id).symbol() == Some("Validate"))
.collect();
single(&reads)
}
fn method_names(graph: &SyntaxGraph, lookup_id: NodeId) -> Vec<&str> {
get_all_scope_definitions(graph, lookup_id)
.into_iter()
.filter_map(|def_id| match node(graph, def_id) {
SyntaxNode::MethodDeclaration { name, .. } => name.as_deref(),
_ => None,
})
.collect()
}
#[test]
fn method_reference_declared_before_use() {
let graph = syntax_graph(
r"
class Example
{
private static bool Validate(object sender)
{
return true;
}
public void Register()
{
Manager.Callback += Validate;
}
}
",
Language::CSharp,
);
assert_eq!(method_names(&graph, method_reference(&graph)), ["Validate"]);
}
#[test]
fn method_reference_declared_after_use() {
let graph = syntax_graph(
r"
class Example
{
public void Register()
{
Manager.Callback += Validate;
}
private static bool Validate(object sender)
{
return true;
}
}
",
Language::CSharp,
);
assert_eq!(method_names(&graph, method_reference(&graph)), ["Validate"]);
}
#[test]
fn method_reference_with_overloads_is_ambiguous() {
let graph = syntax_graph(
r"
class Example
{
public void Register()
{
Manager.Callback += Validate;
}
private static bool Validate(object sender)
{
return true;
}
private static bool Validate(object sender, object cert)
{
return true;
}
}
",
Language::CSharp,
);
assert_eq!(
get_all_scope_definitions(&graph, method_reference(&graph)),
[]
);
}
#[test]
fn method_reference_not_declared_in_file() {
let graph = syntax_graph(
r"
class Example
{
public void Register()
{
Manager.Callback += Validate;
}
}
",
Language::CSharp,
);
assert_eq!(
get_all_scope_definitions(&graph, method_reference(&graph)),
[]
);
}
#[test]
fn variable_read_before_declaration_stays_unresolved() {
let graph = syntax_graph(
r"
class Example
{
public void Register()
{
Use(Validate);
var Validate = Source();
}
}
",
Language::CSharp,
);
let read_id = read_inside(&graph, invocation(&graph, "Use"), "Validate");
assert_eq!(get_all_scope_definitions(&graph, read_id), []);
}