use std::collections::BTreeSet;
use super::python::{keyword, spec};
use crate::string_mask::starts_in_string_mask;
#[derive(Debug, PartialEq)]
enum Kind {
Defines(String),
Neutral,
Runs,
}
#[derive(Debug)]
struct Statement {
first: usize,
last: usize,
kind: Kind,
}
fn is_ident(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
fn ident_of(text: &str) -> &str {
&text[..text.find(|c| !is_ident(c)).unwrap_or(text.len())]
}
fn is_silent(line: &str) -> bool {
let line = line.trim_start();
line.is_empty() || line.starts_with('#')
}
fn carries_on(line: &str) -> bool {
line.starts_with([')', ']', '}'])
|| ["else", "elif", "except", "finally"]
.iter()
.any(|word| keyword(line, word).is_some())
}
fn only_imports(body: &[&str]) -> bool {
body.iter()
.map(|line| line.trim())
.filter(|line| !is_silent(line) && !carries_on(line))
.all(|line| {
["import", "from", "pass"]
.iter()
.any(|word| keyword(line, word).is_some() || line == *word)
})
}
fn assigned(line: &str) -> Option<&str> {
let name = ident_of(line);
let rest = line[name.len()..].trim_start();
let assigns = rest.starts_with(':')
|| rest.starts_with('=') && !rest.starts_with("==")
|| rest
.strip_prefix(|c: char| "+-*/%|&^@".contains(c))
.is_some_and(|after| after.starts_with('='));
(!name.is_empty() && assigns).then_some(name)
}
fn kind_of(head: &str, body: &[&str]) -> Kind {
let defined = keyword(head, "async").map_or(head, str::trim_start);
for word in ["def", "class"] {
if let Some(rest) = keyword(defined, word) {
return Kind::Defines(ident_of(rest.trim_start()).to_string());
}
}
let script = head.starts_with("if __name__");
let imports = keyword(head, "import").is_some() || keyword(head, "from").is_some();
let chooses = (keyword(head, "if").is_some() || head.starts_with("try:")) && only_imports(body);
if script || imports || chooses || head.starts_with(['"', '\'']) {
return Kind::Neutral;
}
assigned(head).map_or(Kind::Runs, |name| Kind::Defines(name.to_string()))
}
fn statements(lines: &[&str]) -> Vec<Statement> {
let Some(spec) = spec() else {
return Vec::new();
};
let owned: Vec<String> = lines.iter().map(|line| line.to_string()).collect();
let in_string = starts_in_string_mask(&owned, spec);
let starts_one = |at: usize| {
let line = lines[at];
!in_string[at]
&& !is_silent(line)
&& !line.starts_with(char::is_whitespace)
&& !carries_on(line)
};
let mut starts: Vec<usize> = Vec::new();
let mut decorating = false;
for at in (0..lines.len()).filter(|&at| starts_one(at)) {
if !decorating {
starts.push(at);
}
decorating = lines[at].starts_with('@');
}
let ends = starts.iter().skip(1).copied().chain([lines.len()]);
starts
.iter()
.zip(ends)
.map(|(&first, end)| {
let head = (first..end)
.find(|&at| starts_one(at) && !lines[at].starts_with('@'))
.unwrap_or(first);
Statement {
first: first + 1,
last: end,
kind: kind_of(lines[head], &lines[head + 1..end]),
}
})
.collect()
}
fn uses_of<'a>(text: &'a str, name: &'a str) -> impl Iterator<Item = (&'a str, &'a str)> {
text.match_indices(name)
.map(move |(at, _)| (&text[..at], &text[at + name.len()..]))
.filter(|(before, after)| {
!before.ends_with(|c| is_ident(c) || c == '.') && !after.starts_with(is_ident)
})
}
pub fn mentions(text: &str, name: &str) -> bool {
uses_of(text, name).any(|(before, _)| !before.ends_with("def ") && !before.ends_with("class "))
}
fn name_of(statement: &Statement) -> Option<&str> {
match &statement.kind {
Kind::Defines(name) => Some(name),
_ => None,
}
}
pub fn defined(source: &str) -> Vec<String> {
let lines: Vec<&str> = source.lines().collect();
let statements = statements(&lines);
let names: BTreeSet<&str> = statements.iter().filter_map(name_of).collect();
names.into_iter().map(str::to_string).collect()
}
fn touched<'a>(
statements: &'a [Statement],
text: &[&str],
lines: &[usize],
) -> Result<BTreeSet<&'a str>, usize> {
let mut touched = BTreeSet::new();
for &line in lines {
let holder = statements
.iter()
.find(|s| (s.first..=s.last).contains(&line));
let Some(statement) =
holder.filter(|s| !text[line - 1..s.last].iter().all(|l| is_silent(l)))
else {
continue;
};
if statement.kind == Kind::Runs {
return Err(line);
}
touched.extend(name_of(statement));
}
Ok(touched)
}
pub fn changed(source: &str, lines: &[usize]) -> Result<BTreeSet<String>, usize> {
let text: Vec<&str> = source.lines().collect();
let statements = statements(&text);
let mut changed = touched(&statements, &text, lines)?;
let bodies: Vec<(&str, String)> = statements
.iter()
.filter_map(|s| Some((name_of(s)?, text[s.first - 1..s.last].join("\n"))))
.collect();
loop {
let users: Vec<&str> = bodies
.iter()
.filter(|(name, _)| !changed.contains(name))
.filter(|(_, body)| changed.iter().any(|name| mentions(body, name)))
.map(|(name, _)| *name)
.collect();
if users.is_empty() {
break;
}
changed.extend(users);
}
Ok(changed.into_iter().map(str::to_string).collect())
}
#[derive(Debug, Default, PartialEq)]
pub struct Taken {
pub names: BTreeSet<String>,
pub whole: bool,
}
pub fn taken(code: &[String], bound: &str) -> Taken {
let mut taken = Taken::default();
for (_, after) in code.iter().flat_map(|line| uses_of(line, bound)) {
let read = after.strip_prefix('.').map(ident_of);
match read.filter(|name| !name.is_empty()) {
Some(name) => {
taken.names.insert(name.to_string());
}
None => taken.whole = true,
}
}
taken
}
#[cfg(test)]
#[path = "python_names_test.rs"]
mod tests;