super-tool 0.14.0

The Super CLI for verifying software behavior
use std::collections::HashSet;

use anyhow::{Result, bail};
use tree_sitter::Node;

use super::{named_children, parse_tree, row_to_line};

#[derive(Default)]
struct Imports<'a> {
    direct: HashSet<&'a str>,
    namespace: HashSet<&'a str>,
}

fn esm_imports<'a>(root: Node<'_>, source: &'a str) -> Imports<'a> {
    let mut imports = Imports::default();
    for import in named_children(root).filter(|node| node.kind() == "import_statement") {
        if !import
            .child_by_field_name("source")
            .is_some_and(|node| authoring_package(node, source))
        {
            continue;
        }
        for clause in named_children(import).filter(|node| node.kind() == "import_clause") {
            for binding in named_children(clause) {
                if binding.kind() == "namespace_import" {
                    if let Some(name) = named_children(binding).find(|node| node.kind() == "identifier") {
                        imports.namespace.insert(&source[name.byte_range()]);
                    }
                } else if binding.kind() == "named_imports" {
                    for specifier in named_children(binding).filter(|node| node.kind() == "import_specifier") {
                        if let Some(name) = specifier.child_by_field_name("name")
                            && &source[name.byte_range()] == "supertest"
                        {
                            let alias = specifier.child_by_field_name("alias").unwrap_or(name);
                            imports.direct.insert(&source[alias.byte_range()]);
                        }
                    }
                }
            }
        }
    }
    imports
}

fn commonjs_imports<'a>(bindings: &[Node<'_>], source: &'a str, imports: &mut Imports<'a>) {
    for binding in bindings {
        let (Some(name), Some(value)) = (
            binding.child_by_field_name("name"),
            binding.child_by_field_name("value"),
        ) else {
            continue;
        };
        if !is_require(value, source) {
            continue;
        }
        match name.kind() {
            "identifier" => {
                imports.namespace.insert(&source[name.byte_range()]);
            }
            "object_pattern" => {
                for property in named_children(name) {
                    if property.kind() == "shorthand_property_identifier_pattern"
                        && &source[property.byte_range()] == "supertest"
                    {
                        imports.direct.insert(&source[property.byte_range()]);
                    } else if property.kind() == "pair_pattern"
                        && let (Some(key), Some(value)) = (
                            property.child_by_field_name("key"),
                            property.child_by_field_name("value"),
                        )
                        && &source[key.byte_range()] == "supertest"
                        && value.kind() == "identifier"
                    {
                        imports.direct.insert(&source[value.byte_range()]);
                    }
                }
            }
            _ => {}
        }
    }
}

pub(super) fn declarations(source: &str) -> Result<Vec<(String, u32)>> {
    let tree = parse_tree(&tree_sitter_javascript::LANGUAGE.into(), source)?;
    let root = tree.root_node();
    let bindings: Vec<_> = named_children(root)
        .flat_map(|node| {
            if node.kind() == "export_statement" {
                named_children(node).collect::<Vec<_>>()
            } else {
                vec![node]
            }
        })
        .filter(|node| matches!(node.kind(), "lexical_declaration" | "variable_declaration"))
        .flat_map(named_children)
        .filter(|node| node.kind() == "variable_declarator")
        .collect();
    let mut imports = esm_imports(root, source);
    commonjs_imports(&bindings, source, &mut imports);
    let mut declarations = Vec::new();
    for binding in bindings {
        let (Some(name), Some(value)) = (
            binding.child_by_field_name("name"),
            binding.child_by_field_name("value"),
        ) else {
            continue;
        };
        if value.kind() != "call_expression" {
            continue;
        }
        let Some(function) = value.child_by_field_name("function") else {
            continue;
        };
        let marked = match function.kind() {
            "identifier" => imports.direct.contains(&source[function.byte_range()]),
            "member_expression" => {
                match (
                    function.child_by_field_name("object"),
                    function.child_by_field_name("property"),
                ) {
                    (Some(object), Some(property)) => {
                        object.kind() == "identifier"
                            && imports.namespace.contains(&source[object.byte_range()])
                            && &source[property.byte_range()] == "supertest"
                    }
                    _ => false,
                }
            }
            _ => false,
        };
        if marked {
            if name.kind() != "identifier" {
                bail!("a JavaScript supertest must be assigned to a single named binding");
            }
            let arguments = value.child_by_field_name("arguments").expect("call arguments");
            if named_children(arguments)
                .filter(|node| node.kind() != "comment")
                .count()
                != 1
            {
                bail!("a JavaScript supertest requires exactly one function argument");
            }
            declarations.push((
                source[name.byte_range()].into(),
                row_to_line(binding.start_position().row),
            ));
        }
    }
    Ok(declarations)
}

fn authoring_package(node: Node<'_>, source: &str) -> bool {
    node.kind() == "string"
        && matches!(
            source[node.byte_range()].trim_matches(['\'', '"']),
            "schematic-supertest" | "schematic-supertest/browser"
        )
}

fn is_require(node: Node<'_>, source: &str) -> bool {
    node.kind() == "call_expression"
        && node
            .child_by_field_name("function")
            .is_some_and(|node| node.kind() == "identifier" && &source[node.byte_range()] == "require")
        && node.child_by_field_name("arguments").is_some_and(|arguments| {
            let arguments: Vec<_> = named_children(arguments)
                .filter(|node| node.kind() != "comment")
                .collect();
            arguments.len() == 1 && authoring_package(arguments[0], source)
        })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn finds_esm_import_aliases_namespace_imports_and_named_bindings() {
        let source = "import { supertest as law } from 'schematic-supertest';\nimport * as schematic from 'schematic-supertest/browser';\nexport const first = law((text) => {});\nconst second = schematic.supertest(function (text) {});";
        assert_eq!(
            declarations(source).unwrap(),
            [("first".into(), 3), ("second".into(), 4)]
        );
    }

    #[test]
    fn finds_commonjs_imports() {
        let source = "const { supertest, supertest: law } = require('schematic-supertest');\nconst schematic = require('schematic-supertest');\nconst first = supertest(x => x), second = law(function(x) {});\nconst third = schematic.supertest(x => x);";
        assert_eq!(
            declarations(source).unwrap(),
            [("first".into(), 3), ("second".into(), 3), ("third".into(), 4)]
        );
    }

    #[test]
    fn ignores_other_packages_strings_comments_and_nested_bindings() {
        let source = r#"import { supertest } from 'somewhere-else';
import { supertest as law } from 'schematic-supertest';
const ordinary = supertest(x => x);
const example = "const fake = law(x => x)";
const template = `const fake = law(x => x)`;
// const fake = law(x => x);
function helper() { const nested = law(x => x); }
"#;
        assert!(declarations(source).unwrap().is_empty());
    }

    #[test]
    fn rejects_destructured_supertests_missing_arguments_and_invalid_syntax() {
        for source in [
            "import { supertest } from 'schematic-supertest'; const { law } = supertest(x => x);",
            "import { supertest } from 'schematic-supertest'; const law = supertest();",
            "import { supertest } from 'schematic-supertest'; const law = supertest(};",
        ] {
            assert!(declarations(source).is_err(), "{source}");
        }
    }
}