tree-sitter-stack-graphs 0.10.0

Create stack graphs using tree-sitter parsers
Documentation
// -*- coding: utf-8 -*-
// ------------------------------------------------------------------------------------------------
// Copyright © 2021, stack-graphs authors.
// Licensed under either of Apache License, Version 2.0, or MIT license, at your option.
// Please see the LICENSE-APACHE or LICENSE-MIT files in this distribution for license details.
// ------------------------------------------------------------------------------------------------

use crate::FILE_PATH_VAR;
use once_cell::sync::Lazy;
use pretty_assertions::assert_eq;
use stack_graphs::arena::Handle;
use stack_graphs::graph::File;
use stack_graphs::graph::StackGraph;
use stack_graphs::partial::PartialPaths;
use stack_graphs::stitching::Database;
use stack_graphs::stitching::ForwardPartialPathStitcher;
use stack_graphs::stitching::StitcherConfig;
use std::path::Path;
use std::path::PathBuf;
use tree_sitter_graph::Variables;
use tree_sitter_stack_graphs::test::Test;
use tree_sitter_stack_graphs::BuildError;
use tree_sitter_stack_graphs::NoCancellation;
use tree_sitter_stack_graphs::StackGraphLanguage;

static PATH: Lazy<PathBuf> = Lazy::new(|| PathBuf::from("test.py"));
static TSG: Lazy<String> = Lazy::new(|| {
    r#"
      global ROOT_NODE
      (module) @mod {
          node @mod.lexical_in
          node @mod.lexical_out
          edge @mod.lexical_in -> ROOT_NODE
          edge ROOT_NODE -> @mod.lexical_out
      }
      (module (_)@stmt) @mod {
          node @stmt.lexical_in
          node @stmt.lexical_out
          edge @stmt.lexical_in -> @mod.lexical_in
          edge @mod.lexical_out -> @stmt.lexical_out
      }
      (module (_)@left . (_)@right) {
          edge @right.lexical_in -> @left.lexical_out
      }
      (expression_statement (assignment left:(identifier)@name))@stmt {
          node @name.def
          attr (@name.def) type = "pop_symbol", symbol = (source-text @name), source_node = @name, is_definition
          edge @stmt.lexical_out -> @name.def
      }
      [
        (expression_statement (assignment right:(identifier)@name))@stmt
        (expression_statement (identifier)@name)@stmt
      ] {
          node @name.ref
          attr (@name.ref) type = "push_symbol", symbol = (source-text @name), source_node = @name, is_reference
          edge @name.ref -> @stmt.lexical_in
      }
    "#.to_string()
});
static TSG_WITH_PKG: Lazy<String> = Lazy::new(|| {
    r#"
      global PKG
    "#
    .to_string()
        + &TSG
});

fn build_stack_graph_into(
    graph: &mut StackGraph,
    file: Handle<File>,
    python_source: &str,
    tsg_source: &str,
    globals: &Variables,
) -> Result<(), BuildError> {
    let language =
        StackGraphLanguage::from_str(tree_sitter_python::LANGUAGE.into(), tsg_source).unwrap();
    language.build_stack_graph_into(graph, file, python_source, globals, &NoCancellation)?;
    Ok(())
}

fn check_test(
    python_path: &Path,
    python_source: &str,
    tsg_source: &str,
    expected_successes: usize,
    expected_failures: usize,
) {
    let mut test =
        Test::from_source(python_path, python_source, python_path).expect("Could not parse test");
    let assertion_count: usize = test.fragments.iter().map(|f| f.assertions.len()).sum();
    assert_eq!(
        expected_successes + expected_failures,
        assertion_count,
        "expected {} assertions, got {}",
        expected_successes + expected_failures,
        assertion_count,
    );

    let mut globals = Variables::new();
    for fragments in &test.fragments {
        globals.clear();

        fragments.add_globals_to(&mut globals);

        globals
            .add(
                FILE_PATH_VAR.into(),
                fragments.path.to_str().unwrap().into(),
            )
            .unwrap_or_default();

        build_stack_graph_into(
            &mut test.graph,
            fragments.file,
            &fragments.source,
            tsg_source,
            &globals,
        )
        .expect("Could not load stack graph");
    }
    let mut partials = PartialPaths::new();
    let mut db = Database::new();
    for fragment in &test.fragments {
        ForwardPartialPathStitcher::find_minimal_partial_path_set_in_file(
            &test.graph,
            &mut partials,
            fragment.file,
            StitcherConfig::default(),
            &stack_graphs::NoCancellation,
            |graph, partials, path| {
                db.add_partial_path(graph, partials, path.clone());
            },
        )
        .expect("should nopt be cancelled");
    }

    let results = test
        .run(
            &mut partials,
            &mut db,
            StitcherConfig::default(),
            &NoCancellation,
        )
        .expect("should never be cancelled");
    assert_eq!(
        expected_successes,
        results.success_count(),
        "expected {} successes, got {}",
        expected_successes,
        results.success_count()
    );
    assert_eq!(
        expected_failures,
        results.failure_count(),
        "expected {} failures, got {}",
        expected_failures,
        results.failure_count()
    );
}

#[test]
fn can_assert_defined_on_one_line() {
    let python = r#"
      x = 1;
        x;
      # ^ defined: 2
    "#;
    check_test(&PATH, python, &TSG, 1, 0);
}

#[test]
fn can_assert_defined_on_multiple_lines() {
    let python = r#"
      # --- path: a.py ---
      x = 1;

      # --- path: b.py ---
      x = 1;

      # --- path: c.py ---
        x;
      # ^ defined: 3, 6
    "#;
    check_test(&PATH, python, &TSG, 1, 0);
}

#[test]
fn can_assert_defined_on_no_lines() {
    let python = r#"
      y = 1;
        x;
      # ^ defined:
    "#;
    check_test(&PATH, python, &TSG, 1, 0);
}

#[test]
fn can_assert_defines_one_symbol() {
    let python = r#"
        x = 1;
      # ^ defines: x
    "#;
    check_test(&PATH, python, &TSG, 1, 0);
}

#[test]
fn can_assert_defines_no_symbols() {
    let python = r#"
        x;
      # ^ defines:
    "#;
    check_test(&PATH, python, &TSG, 1, 0);
}

#[test]
fn can_assert_refers_one_symbol() {
    let python = r#"
        x;
      # ^ refers: x
    "#;
    check_test(&PATH, python, &TSG, 1, 0);
}

#[test]
fn can_assert_refers_no_symbols() {
    let python = r#"
        x = 1;
      # ^ refers:
    "#;
    check_test(&PATH, python, &TSG, 1, 0);
}

#[test]
fn test_cannot_use_unknown_assertion() {
    let python = r#"
      foo = 42
      # ^ supercalifragilisticexpialidocious:
    "#;
    if let Ok(_) = Test::from_source(&PATH, python, &PATH) {
        panic!("Parsing test unexpectedly succeeded.");
    }
}
#[test]
fn aligns_correctly_with_unicode() {
    let python = r#"
      x = 1;
      m = {};

      # multi code unit character in assertion line
      m[" "] = x;
      #  §   # ^ defined: 2

      # multi code point character in source line
      m["§"] = x;
      #      # ^ defined: 2

      # multi code point character in assertion line
      m[" "] = x;
      #  g̈   # ^ defined: 2

      # multi code point character in source line
      m["g̈"] = x;
      #      # ^ defined: 2
    "#;
    check_test(&PATH, python, &TSG, 4, 0);
}

#[test]
fn test_can_be_multi_file() {
    let python = r#"
      # --- path: a.py ---
      x = 1;

      # --- path: b.py ---
        x;
      # ^ defined: 3
    "#;
    check_test(&PATH, python, &TSG, 1, 0);
}

#[test]
fn test_fragment_can_have_same_name_as_test() {
    let python = r#"
      # --- path: test.py ---
      x = 1;
        x;
      # ^ defined: 3
    "#;
    check_test(&PathBuf::from("test.py"), python, &TSG, 1, 0);
}

#[test]
fn test_cannot_assert_on_first_line() {
    let python = r#"
      # ^ defined: 3
    "#;
    if let Ok(_) = Test::from_source(&PATH, python, &PATH) {
        panic!("Parsing test unexpectedly succeeded.");
    }
}

#[test]
fn test_cannot_assert_before_first_fragment() {
    let python = r#"
      # this is ignored
      # --- path: a.py ---
        x;
      # ^ defined: 1
    "#;
    if let Ok(_) = Test::from_source(&PATH, python, &PATH) {
        panic!("Parsing test unexpectedly succeeded.");
    }
}

#[test]
fn test_can_set_global() {
    let python = r#"
      # --- global: PKG=test ---
      pass
    "#;
    check_test(&PathBuf::from("test.py"), python, &TSG_WITH_PKG, 0, 0);
}

#[test]
fn test_can_set_global_in_fragments() {
    let python = r#"
      # --- path: a.py ---
      # --- global: PKG=test ---
      pass
      # --- path: b.py ---
      # --- global: PKG=test ---
      pass
    "#;
    check_test(&PathBuf::from("test.py"), python, &TSG_WITH_PKG, 0, 0);
}

#[test]
fn test_cannot_set_global_before_first_fragment() {
    let python = r#"
      # --- global: PKG=test ---
      # --- path: a.py ---
      pass
    "#;
    if let Ok(_) = Test::from_source(&PATH, python, &PATH) {
        panic!("Parsing test unexpectedly succeeded.");
    }
}