use std::sync::Arc;
use harn_vm::VmValue;
use tree_sitter::{Node, Tree};
use crate::tools::args::{build_dict, str_value};
use super::language::Language;
use super::undefined_names::node_text;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ResolutionCeiling {
SingleFileComplete,
PackageScoped,
RuntimeResolved,
}
impl ResolutionCeiling {
fn as_str(self) -> &'static str {
match self {
Self::SingleFileComplete => "single_file_complete",
Self::PackageScoped => "package_scoped",
Self::RuntimeResolved => "runtime_resolved",
}
}
}
pub(super) fn ceiling(language: Language) -> ResolutionCeiling {
match language {
Language::Python
| Language::JavaScript
| Language::Jsx
| Language::TypeScript
| Language::Tsx => ResolutionCeiling::SingleFileComplete,
Language::Go => ResolutionCeiling::PackageScoped,
Language::Ruby => ResolutionCeiling::RuntimeResolved,
_ => ResolutionCeiling::RuntimeResolved,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Defeater {
WildcardImport,
DynamicNameAccess,
DynamicMethodDefinition,
Eval,
WithScope,
SyntaxError,
}
impl Defeater {
fn as_str(self) -> &'static str {
match self {
Self::WildcardImport => "wildcard_import",
Self::DynamicNameAccess => "dynamic_name_access",
Self::DynamicMethodDefinition => "dynamic_method_definition",
Self::Eval => "eval",
Self::WithScope => "with_scope",
Self::SyntaxError => "syntax_error",
}
}
}
#[derive(Debug, Clone)]
pub(super) struct Resolution {
ceiling: ResolutionCeiling,
defeaters: Vec<Defeater>,
analysed: bool,
}
impl Resolution {
pub(super) fn is_complete(&self) -> bool {
self.analysed
&& self.ceiling == ResolutionCeiling::SingleFileComplete
&& self.defeaters.is_empty()
}
pub(super) fn to_vm_value(&self) -> VmValue {
let defeaters: Vec<VmValue> = self
.defeaters
.iter()
.map(|d| str_value(d.as_str()))
.collect();
build_dict([
("complete", VmValue::Bool(self.is_complete())),
("analysed", VmValue::Bool(self.analysed)),
("ceiling", str_value(self.ceiling.as_str())),
("defeaters", VmValue::List(Arc::new(defeaters))),
])
}
pub(super) fn unanalysed(language: Language) -> Self {
Self {
ceiling: ceiling(language),
defeaters: Vec::new(),
analysed: false,
}
}
}
pub(super) fn assess(tree: &Tree, source: &str, language: Language) -> Resolution {
let mut defeaters: Vec<Defeater> = Vec::new();
let mut note = |d: Defeater| {
if !defeaters.contains(&d) {
defeaters.push(d);
}
};
if tree.root_node().has_error() {
note(Defeater::SyntaxError);
}
scan(tree.root_node(), source, language, &mut note);
Resolution {
ceiling: ceiling(language),
defeaters,
analysed: true,
}
}
const PYTHON_DYNAMIC_CALLS: &[&str] = &[
"eval", "exec", "globals", "locals", "setattr", "delattr", "vars", "compile",
];
const RUBY_DYNAMIC_CALLS: &[&str] = &[
"method_missing",
"define_method",
"const_missing",
"instance_variable_set",
"const_set",
"class_eval",
"instance_eval",
"attr_accessor",
"attr_reader",
"attr_writer",
];
fn scan(node: Node<'_>, source: &str, language: Language, note: &mut impl FnMut(Defeater)) {
match (language, node.kind()) {
(Language::Python, "wildcard_import") => note(Defeater::WildcardImport),
(Language::Python, "call") => {
if let Some(name) = called_name(node, source) {
if PYTHON_DYNAMIC_CALLS.contains(&name) {
note(if name == "eval" || name == "exec" || name == "compile" {
Defeater::Eval
} else {
Defeater::DynamicNameAccess
});
}
}
}
(Language::Python, "function_definition") => {
if let Some(name) = node.child_by_field_name("name") {
if matches!(node_text(name, source), "__getattr__" | "__getattribute__") {
note(Defeater::DynamicNameAccess);
}
}
}
(Language::Ruby, "call") => {
if let Some(name) = called_name(node, source) {
if RUBY_DYNAMIC_CALLS.contains(&name) {
note(Defeater::DynamicMethodDefinition);
}
}
}
(Language::Ruby, "method") => {
if let Some(name) = node.child_by_field_name("name") {
if matches!(node_text(name, source), "method_missing" | "const_missing") {
note(Defeater::DynamicMethodDefinition);
}
}
}
(
Language::JavaScript | Language::Jsx | Language::TypeScript | Language::Tsx,
"call_expression",
) => {
if let Some(name) = called_name(node, source) {
if name == "eval" {
note(Defeater::Eval);
}
}
}
(
Language::JavaScript | Language::Jsx | Language::TypeScript | Language::Tsx,
"with_statement",
) => note(Defeater::WithScope),
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
scan(child, source, language, note);
}
}
fn called_name<'a>(node: Node<'_>, source: &'a str) -> Option<&'a str> {
let callee = node
.child_by_field_name("function")
.or_else(|| node.child_by_field_name("method"))?;
match callee.kind() {
"identifier" => Some(node_text(callee, source)),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::parse::parse_source;
fn assess_source(source: &str, language: Language) -> Resolution {
let tree = parse_source(source, language).expect("parses");
assess(&tree, source, language)
}
fn defeater_names(resolution: &Resolution) -> Vec<&'static str> {
resolution.defeaters.iter().map(|d| d.as_str()).collect()
}
#[test]
fn plain_python_file_resolves_completely() {
let resolution =
assess_source("import os\n\n\ndef f():\n return os\n", Language::Python);
assert!(resolution.is_complete());
assert!(defeater_names(&resolution).is_empty());
}
#[test]
fn python_wildcard_import_defeats_resolution() {
let resolution = assess_source(
"from constants import *\n\n\ndef f():\n return MAX\n",
Language::Python,
);
assert!(!resolution.is_complete());
assert_eq!(defeater_names(&resolution), vec!["wildcard_import"]);
}
#[test]
fn python_dynamic_name_calls_defeat_resolution() {
let resolution = assess_source("def f(t):\n setattr(t, 'a', 1)\n", Language::Python);
assert!(!resolution.is_complete());
assert_eq!(defeater_names(&resolution), vec!["dynamic_name_access"]);
}
#[test]
fn python_module_getattr_defeats_resolution() {
let resolution = assess_source(
"def __getattr__(name):\n return name\n",
Language::Python,
);
assert!(!resolution.is_complete());
assert_eq!(defeater_names(&resolution), vec!["dynamic_name_access"]);
}
#[test]
fn python_method_named_like_a_builtin_is_not_a_defeater() {
let resolution = assess_source("def f(o):\n o.setattr('a', 1)\n", Language::Python);
assert!(resolution.is_complete());
}
#[test]
fn ruby_is_never_complete_even_when_plainly_written() {
let resolution = assess_source("def total(a)\n a + 1\nend\n", Language::Ruby);
assert!(!resolution.is_complete());
assert_eq!(resolution.ceiling.as_str(), "runtime_resolved");
assert!(defeater_names(&resolution).is_empty());
}
#[test]
fn ruby_dynamic_definition_is_named_as_a_defeater() {
let resolution = assess_source(
"class A\n def method_missing(n)\n n\n end\nend\n",
Language::Ruby,
);
assert!(!resolution.is_complete());
assert_eq!(
defeater_names(&resolution),
vec!["dynamic_method_definition"]
);
}
#[test]
fn go_is_package_scoped_because_sibling_files_contribute_names() {
let resolution = assess_source("package main\n\nfunc f() int { return 1 }\n", Language::Go);
assert!(!resolution.is_complete());
assert_eq!(resolution.ceiling.as_str(), "package_scoped");
}
#[test]
fn javascript_eval_defeats_resolution() {
let resolution = assess_source("function f(s) { return eval(s); }\n", Language::JavaScript);
assert!(!resolution.is_complete());
assert_eq!(defeater_names(&resolution), vec!["eval"]);
}
#[test]
fn plain_javascript_file_resolves_completely() {
let resolution = assess_source(
"export function f(a) { return a + 1; }\n",
Language::JavaScript,
);
assert!(resolution.is_complete());
}
#[test]
fn defeaters_are_reported_once_each() {
let resolution = assess_source(
"from a import *\nfrom b import *\n\n\ndef f(t):\n setattr(t, 'x', 1)\n setattr(t, 'y', 2)\n",
Language::Python,
);
assert_eq!(
defeater_names(&resolution),
vec!["wildcard_import", "dynamic_name_access"]
);
}
#[test]
fn unanalysed_file_is_never_complete() {
assert!(!Resolution::unanalysed(Language::Python).is_complete());
}
#[test]
fn a_file_that_does_not_parse_cleanly_is_not_complete() {
let resolution = assess_source("def f(:\n return zzz\n", Language::Python);
assert!(!resolution.is_complete());
assert!(defeater_names(&resolution).contains(&"syntax_error"));
}
}