diff --git a/.github/workflows/publish-tree-sitter-stack-graphs-ruby.yml b/.github/workflows/publish-tree-sitter-stack-graphs-ruby.yml new file mode 100644 index 000000000..c2d11571b --- /dev/null +++ b/.github/workflows/publish-tree-sitter-stack-graphs-ruby.yml @@ -0,0 +1,45 @@ +name: Publish tree-sitter-stack-graphs-ruby release + +on: + push: + tags: + - tree-sitter-stack-graphs-ruby-v* + +jobs: + publish-crate: + runs-on: ubuntu-latest + env: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + CRATE_DIR: './languages/tree-sitter-stack-graphs-ruby' + steps: + - name: Install Rust environment + uses: hecrj/setup-rust-action@v1 + - name: Checkout repository + uses: actions/checkout@v3 + # TODO Verify the crate version matches the tag + - name: Test crate + run: cargo test --all-features + working-directory: ${{ env.CRATE_DIR }} + - name: Verify publish crate + run: cargo publish --dry-run + working-directory: ${{ env.CRATE_DIR }} + - name: Publish crate + run: cargo publish + working-directory: ${{ env.CRATE_DIR }} + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + create-release: + needs: publish-crate + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Create GitHub release + uses: ncipollo/release-action@v1 + with: + body: | + Find more info on all releases at https://crates.io/crates/tree-sitter-stack-graphs-ruby. + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/languages/tree-sitter-stack-graphs-ruby/.gitignore b/languages/tree-sitter-stack-graphs-ruby/.gitignore new file mode 100644 index 000000000..faf6459fb --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/.gitignore @@ -0,0 +1,3 @@ +*.html +/Cargo.lock +/target diff --git a/languages/tree-sitter-stack-graphs-ruby/CHANGELOG.md b/languages/tree-sitter-stack-graphs-ruby/CHANGELOG.md new file mode 100644 index 000000000..e75f8bab3 --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog for tree-sitter-stack-graphs-ruby + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). diff --git a/languages/tree-sitter-stack-graphs-ruby/Cargo.toml b/languages/tree-sitter-stack-graphs-ruby/Cargo.toml new file mode 100644 index 000000000..f91eda976 --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "tree-sitter-stack-graphs-ruby" +version = "0.1.0" +description = "Stack graphs definition for Ruby using tree-sitter-ruby" +readme = "README.md" +keywords = ["tree-sitter", "stack-graphs", "ruby"] +authors = [ + "GitHub ", +] +license = "MIT OR Apache-2.0" +edition = "2018" + +[[bin]] +name = "tree-sitter-stack-graphs-ruby" +path = "rust/bin.rs" +required-features = ["cli"] + +[lib] +path = "rust/lib.rs" +test = false + +[[test]] +name = "test" +path = "rust/test.rs" +harness = false + +[features] +cli = ["anyhow", "clap", "tree-sitter-stack-graphs/cli"] + +[dependencies] +anyhow = { version = "1.0", optional = true } +clap = { version = "4", optional = true, features = ["derive"] } +tree-sitter-graph = { version = "0.10.0" } +tree-sitter-stack-graphs = { version = "0.6.0", path = "../../tree-sitter-stack-graphs" } +tree-sitter-ruby = "0.20.0" +uuid = { version = "1.3", features = ["v4", "fast-rng"] } + +[dev-dependencies] +anyhow = "1.0" +tree-sitter-stack-graphs = { version = "0.6.0", path = "../../tree-sitter-stack-graphs", features = ["cli"] } diff --git a/languages/tree-sitter-stack-graphs-ruby/README.md b/languages/tree-sitter-stack-graphs-ruby/README.md new file mode 100644 index 000000000..ea44d2c0c --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/README.md @@ -0,0 +1,98 @@ +# tree-sitter-stack-graphs definition for Ruby + +This project defines tree-sitter-stack-graphs rules for Ruby using the [tree-sitter-ruby][] grammar. + +[tree-sitter-ruby]: https://crates.io/crates/tree-sitter-ruby + +## Usage + +To use this library, add the following to your `Cargo.toml`: + +``` toml +[dependencies] +tree-sitter-stack-graphs-ruby = "0.0.1" +``` + +Check out our [documentation](https://docs.rs/tree-sitter-stack-graphs-ruby/*/) for more details on how to use this library. + +## Command-line Program + +The command-line program for `tree-sitter-stack-graphs-ruby` lets you do stack graph based analysis and lookup from the command line. + +Install the program using `cargo install` as follows: + +``` sh +$ cargo install --features cli tree-sitter-stack-graphs-ruby +$ tree-sitter-stack-graphs-ruby --help +``` + +## Development + +The project is written in Rust, and requires a recent version installed. Rust can be installed and updated using [rustup][]. + +[rustup]: https://rustup.rs/ + +The project is organized as follows: + +- The stack graph rules are defined in `src/stack-graphs.tsg`. +- Builtins sources and configuration are defined in `src/builtins.rb` and `builtins.cfg` respectively. +- Tests are put into the `test` directory. + +### Building and Running Tests + +Build the project by running: + +``` sh +$ cargo build +``` + +Run the tests as follows: + +``` sh +$ cargo test +``` + +The project consists of a library and a CLI. By default, running `cargo` only applies to the library. To run `cargo` commands on the CLI as well, add `--features cli` or `--all-features`. + +Run the CLI from source as follows: + +``` sh +$ cargo run --features cli -- ARGS +``` + +Sources are formatted using the standard Rust formatted, which is applied by running: + +``` sh +$ cargo fmt +``` + +### Writing TSG + +The stack graph rules are written in [tree-sitter-graph][]. Checkout the [examples][], +which contain self-contained TSG rules for specific language features. A VSCode +[extension][] is available that provides syntax highlighting for TSG files. + +[tree-sitter-graph]: https://github.com/tree-sitter/tree-sitter-graph +[examples]: https://github.com/github/stack-graphs/blob/main/tree-sitter-stack-graphs/examples/ +[extension]: https://marketplace.visualstudio.com/items?itemName=tree-sitter.tree-sitter-graph + +Parse and test a single file by executing the following commands: + +``` sh +$ cargo run --features cli -- parse FILES... +$ cargo run --features cli -- test TESTFILES... +``` + +Generate a visualization to debug failing tests by passing the `-V` flag: + +``` sh +$ cargo run --features cli -- test -V TESTFILES... +``` + +To generate the visualization regardless of test outcome, execute: + +``` sh +$ cargo run --features cli -- test -V --output-mode=always TESTFILES... +``` + +Go to https://crates.io/crates/tree-sitter-stack-graphs for links to examples and documentation. diff --git a/languages/tree-sitter-stack-graphs-ruby/contrib/match b/languages/tree-sitter-stack-graphs-ruby/contrib/match new file mode 100755 index 000000000..1774cc440 --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/contrib/match @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -eu + +cargo run --features cli -- match "$@" diff --git a/languages/tree-sitter-stack-graphs-ruby/contrib/parse b/languages/tree-sitter-stack-graphs-ruby/contrib/parse new file mode 100755 index 000000000..f7fffd677 --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/contrib/parse @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -eu + +cargo run --features cli -- parse "$@" diff --git a/languages/tree-sitter-stack-graphs-ruby/contrib/test b/languages/tree-sitter-stack-graphs-ruby/contrib/test new file mode 100755 index 000000000..2cdadde9c --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/contrib/test @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -eu + +cargo run --features cli -- test -V=test.html --output-mode always "$@" diff --git a/languages/tree-sitter-stack-graphs-ruby/doc/refinement.png b/languages/tree-sitter-stack-graphs-ruby/doc/refinement.png new file mode 100644 index 000000000..8705871ec Binary files /dev/null and b/languages/tree-sitter-stack-graphs-ruby/doc/refinement.png differ diff --git a/languages/tree-sitter-stack-graphs-ruby/example.rb b/languages/tree-sitter-stack-graphs-ruby/example.rb new file mode 100644 index 000000000..de1c4868c --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/example.rb @@ -0,0 +1,36 @@ +module A + CONST = 1 + def a; end +end + +module B + include A + + def b; end +end + +include B + +C = Module.new do + def c; end + def b + puts "c" + end +end + +self.extend(C) + +class D + include C + + def calling_a + puts "calling a: #{a}" + end + + def calling_c + puts "calling c: #{c}" + end +end + +d = D.new +d.b # => "c" diff --git a/languages/tree-sitter-stack-graphs-ruby/rust/bin.rs b/languages/tree-sitter-stack-graphs-ruby/rust/bin.rs new file mode 100644 index 000000000..35a96acbb --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/rust/bin.rs @@ -0,0 +1,32 @@ +// -*- coding: utf-8 -*- +// ------------------------------------------------------------------------------------------------ +// Copyright © 2023, 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 anyhow::anyhow; +use clap::Parser; +use tree_sitter_stack_graphs::cli::database::default_user_database_path_for_crate; +use tree_sitter_stack_graphs::cli::provided_languages::Subcommands; +use tree_sitter_stack_graphs::NoCancellation; + +fn main() -> anyhow::Result<()> { + let lc = match tree_sitter_stack_graphs_ruby::try_language_configuration(&NoCancellation) { + Ok(lc) => lc, + Err(err) => { + eprintln!("{}", err.display_pretty()); + return Err(anyhow!("Language configuration error")); + } + }; + let cli = Cli::parse(); + let default_db_path = default_user_database_path_for_crate(env!("CARGO_PKG_NAME"))?; + cli.subcommand.run(default_db_path, vec![lc]) +} + +#[derive(Parser)] +#[clap(about, version)] +pub struct Cli { + #[clap(subcommand)] + subcommand: Subcommands, +} diff --git a/languages/tree-sitter-stack-graphs-ruby/rust/lib.rs b/languages/tree-sitter-stack-graphs-ruby/rust/lib.rs new file mode 100644 index 000000000..baede5e6c --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/rust/lib.rs @@ -0,0 +1,66 @@ +// -*- coding: utf-8 -*- +// ------------------------------------------------------------------------------------------------ +// Copyright © 2023, 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 tree_sitter_stack_graphs::loader::FileAnalyzers; +use tree_sitter_stack_graphs::loader::LanguageConfiguration; +use tree_sitter_stack_graphs::loader::LoadError; +use tree_sitter_stack_graphs::CancellationFlag; + +/// The stack graphs tsg source for this language. +pub const STACK_GRAPHS_TSG_PATH: &str = "src/stack-graphs.tsg"; +/// The stack graphs tsg source for this language. +pub const STACK_GRAPHS_TSG_SOURCE: &str = include_str!("../src/stack-graphs.tsg"); + +/// The stack graphs builtins configuration for this language. +pub const STACK_GRAPHS_BUILTINS_CONFIG: &str = include_str!("../src/builtins.cfg"); +/// The stack graphs builtins path for this language +pub const STACK_GRAPHS_BUILTINS_PATH: &str = "src/builtins.rb"; +/// The stack graphs builtins source for this language. +pub const STACK_GRAPHS_BUILTINS_SOURCE: &str = include_str!("../src/builtins.rb"); + +/// The name of the file path global variable. +pub const FILE_PATH_VAR: &str = "FILE_PATH"; + +pub fn language_configuration(cancellation_flag: &dyn CancellationFlag) -> LanguageConfiguration { + try_language_configuration(cancellation_flag).unwrap_or_else(|err| panic!("{}", err)) +} + +pub fn try_language_configuration( + cancellation_flag: &dyn CancellationFlag, +) -> Result { + let mut lc = LanguageConfiguration::from_sources( + tree_sitter_ruby::language(), + Some(String::from("source.rb")), + None, + vec![String::from("rb")], + STACK_GRAPHS_TSG_PATH.into(), + STACK_GRAPHS_TSG_SOURCE, + Some(( + STACK_GRAPHS_BUILTINS_PATH.into(), + STACK_GRAPHS_BUILTINS_SOURCE, + )), + Some(STACK_GRAPHS_BUILTINS_CONFIG), + FileAnalyzers::new(), + cancellation_flag, + )?; + lc.sgl.functions_mut().add("uuid".into(), UUID); + Ok(lc) +} + +struct UUID; + +impl tree_sitter_graph::functions::Function for UUID { + fn call( + &self, + _graph: &mut tree_sitter_graph::graph::Graph, + _source: &str, + parameters: &mut dyn tree_sitter_graph::functions::Parameters, + ) -> Result { + parameters.finish()?; + Ok(uuid::Uuid::new_v4().to_string().into()) + } +} diff --git a/languages/tree-sitter-stack-graphs-ruby/rust/test.rs b/languages/tree-sitter-stack-graphs-ruby/rust/test.rs new file mode 100644 index 000000000..1b3109cc0 --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/rust/test.rs @@ -0,0 +1,23 @@ +// -*- coding: utf-8 -*- +// ------------------------------------------------------------------------------------------------ +// Copyright © 2023, 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 anyhow::anyhow; +use std::path::PathBuf; +use tree_sitter_stack_graphs::ci::Tester; +use tree_sitter_stack_graphs::NoCancellation; + +fn main() -> anyhow::Result<()> { + let lc = match tree_sitter_stack_graphs_ruby::try_language_configuration(&NoCancellation) { + Ok(lc) => lc, + Err(err) => { + eprintln!("{}", err.display_pretty()); + return Err(anyhow!("Language configuration error")); + } + }; + let test_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test"); + Tester::new(vec![lc], vec![test_path]).run() +} diff --git a/languages/tree-sitter-stack-graphs-ruby/src/builtins.cfg b/languages/tree-sitter-stack-graphs-ruby/src/builtins.cfg new file mode 100644 index 000000000..d685061be --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/src/builtins.cfg @@ -0,0 +1 @@ +[globals] diff --git a/languages/tree-sitter-stack-graphs-ruby/src/builtins.rb b/languages/tree-sitter-stack-graphs-ruby/src/builtins.rb new file mode 100644 index 000000000..e69de29bb diff --git a/languages/tree-sitter-stack-graphs-ruby/src/stack-graphs.tsg b/languages/tree-sitter-stack-graphs-ruby/src/stack-graphs.tsg new file mode 100644 index 000000000..6a04f3477 --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/src/stack-graphs.tsg @@ -0,0 +1,469 @@ +;; -*- coding: utf-8 -*- +;; ------------------------------------------------------------------------------------------------ +;; Copyright © 2023, 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. +;; ------------------------------------------------------------------------------------------------ + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Stack graphs definition for Ruby +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; Global Variables +;; ^^^^^^^^^^^^^^^^ + +global FILE_PATH +global ROOT_NODE +global JUMP_TO_SCOPE_NODE + +;; Attribute Shorthands +;; ^^^^^^^^^^^^^^^^^^^^ + +attribute node_definition = node => type = "pop_symbol", node_symbol = node, is_definition +attribute node_reference = node => type = "push_symbol", node_symbol = node, is_reference +attribute pop_node = node => type = "pop_symbol", node_symbol = node +attribute pop_scoped_node = node => type = "pop_scoped_symbol", node_symbol = node +attribute pop_scoped_symbol = symbol => type = "pop_scoped_symbol", symbol = symbol +attribute pop_symbol = symbol => type = "pop_symbol", symbol = symbol +attribute push_node = node => type = "push_symbol", node_symbol = node +attribute push_scoped_node = node => type = "push_scoped_symbol", node_symbol = node +attribute push_scoped_symbol = symbol => type = "push_scoped_symbol", symbol = symbol +attribute push_symbol = symbol => type = "push_symbol", symbol = symbol +attribute scoped_node_definition = node => type = "pop_scoped_symbol", node_symbol = node, is_definition +attribute scoped_node_reference = node => type = "push_scoped_symbol", node_symbol = node, is_reference +attribute symbol_definition = symbol => type = "pop_symbol", symbol = symbol, is_definition +attribute symbol_reference = symbol => type = "push_symbol", symbol = symbol, is_reference + +attribute node_symbol = node => symbol = (source-text node), source_node = node + +;; Program +;; ^^^^^^^ + +(program)@prog { + ;; program lexical scope inherits global scope + edge @prog.lexical_scope -> ROOT_NODE + + ;; program lexical scope inherits global scope via the root + ;; scope marker <::> + node root_scope + attr (root_scope) pop_symbol = "<::>" + edge root_scope -> ROOT_NODE + edge @prog.lexical_scope -> root_scope + + ;; top-level definitions are accessible from the global scope + edge ROOT_NODE -> @prog.instance_defs + edge ROOT_NODE -> @prog.root_defs + edge ROOT_NODE -> @prog.static_defs +} + +;; Bodies + +[ + (program) + (body_statement) +]@body { + node @body.instance_defs + node @body.lexical_scope + node @body.root_defs + node @body.static_defs + + edge @body.lexical_scope -> @body.static_defs + attr (@body.lexical_scope -> @body.static_defs) precedence = 1 +} + +[ + (program (_)@stmt)@body + (body_statement (_)@stmt)@body +] { + ;; program inherits statement definitions + edge @body.instance_defs -> @stmt.instance_defs + edge @body.root_defs -> @stmt.root_defs + edge @body.static_defs -> @stmt.static_defs +} + +[ + (program . (_)@first)@body + (body_statement . (_)@first)@body +] { + edge @first.lexical_scope -> @body.lexical_scope +} + +[ + (program (_)@left . (_)@right) + (body_statement (_)@left . (_)@right) +] { + edge @right.lexical_scope -> @left.lexical_scope + edge @right.lexical_scope -> @left.local_defs + attr (@right.lexical_scope -> @left.local_defs) precedence = 1 +} + +;; Statements +;; ^^^^^^^^^^ + +[ + (class) + (module) + (uninterpreted) +]@stmt { + node @stmt.instance_defs + node @stmt.lexical_scope + node @stmt.local_defs + node @stmt.root_defs + node @stmt.static_defs +} + +[ + (class name:(_)@name) + (module name:(_)@name) +]@class { + ;; expose the class definition + edge @class.root_defs -> @name.root_def + edge @class.static_defs -> @name.static_def + + ;; resolve the class in the lexical scope + edge @name.ref -> @class.lexical_scope +} + +(class)@class { + node @class.instance +} + +(class name:(_)@name)@class { + node dot + attr (dot) pop_symbol = "." + + node def + attr (def) pop_symbol = "new" + + edge @name.def_value -> dot + edge dot -> def + edge def -> @class.instance +} + +(class body:(_)@body)@class { + edge @class.instance -> @body.instance_defs +} + +(class name:(_)@name)@class { + let nonce = (uuid) + + node refine_in + attr (refine_in) pop_scoped_symbol = "" + ; + node push_nonce + attr (push_nonce) push_symbol = nonce + ; + node using + attr (using) push_symbol = "" + ; + edge @class.instance -> refine_in + edge refine_in -> push_nonce + edge push_nonce -> using + edge using -> JUMP_TO_SCOPE_NODE + + node refine + attr (refine) pop_scoped_symbol = "" + ; + node pop_nonce + attr (pop_nonce) pop_symbol = nonce + ; + edge @name.def_value -> refine + edge refine -> pop_nonce + edge pop_nonce -> JUMP_TO_SCOPE_NODE +} + +[ + (class name:(_)@name body:(_)@body) + (module name:(_)@name body:(_)@body) +]@class { + ;; body inherits class lexical scope + edge @body.lexical_scope -> @class.lexical_scope + + ;; body inherits other definitions of this class + edge @body.lexical_scope -> @name.ref_value + + ;; class body consists of body definitions + edge @name.def_value -> @body.static_defs + + ;; root definitions are exposed upwards + edge @class.root_defs -> @body.root_defs +} + +;; Expressions +;; ^^^^^^^^^^^ + +[ + (_primary/constant) + (_primary/identifier) + (_primary/scope_resolution) + (assignment) + (call) + (integer) + (method) + (singleton_method) +]@expr { + node @expr.instance_defs + node @expr.lexical_scope + node @expr.local_defs + node @expr.root_defs + node @expr.static_defs + node @expr.value +} + +[ + (_primary/constant) + (_primary/identifier) + (_primary/scope_resolution) +]@name { + edge @name.ref -> @name.lexical_scope +} + +[ + (_primary/constant) + (_primary/identifier) + (_primary/scope_resolution) +]@name { + node pop_dot + attr (pop_dot) pop_symbol = "." + + node push_dot + attr (push_dot) push_symbol = "." + + ;; The value of the expression is the value if the reference, but only + ;; if we are trying to look _inside_ the value. This prevents assignments + ;; of the form `foo = bar` to act as aliases. + edge @name.value -> pop_dot + edge pop_dot -> push_dot + edge push_dot -> @name.ref_value +} + +[ + (_primary/constant) + (_primary/scope_resolution) +]@name { + node pop_double_colon + attr (pop_double_colon) pop_symbol = "::" + + node push_double_colon + attr (push_double_colon) push_symbol = "::" + + ;; The value of the expression is the value if the reference, but only + ;; if we are trying to look _inside_ the value. This prevents assignments + ;; of the form `foo = bar` to act as aliases. + edge @name.value -> pop_double_colon + edge pop_double_colon -> push_double_colon + edge push_double_colon -> @name.ref_value +} + +(assignment left:(_)@name right:(_)@value)@assign { + edge @value.lexical_scope -> @assign.lexical_scope + + edge @assign.local_defs -> @name.local_def + edge @assign.root_defs -> @name.root_def + edge @assign.static_defs -> @name.static_def + + edge @name.def_value -> @value.value +} + +(call receiver:(_)@recv method:(_)@mthd_name)@call { + node dot + attr (dot) push_symbol = "." + + edge @call.value -> @mthd_name.ref_value + edge @mthd_name.ref -> dot + edge dot -> @recv.value + edge @recv.lexical_scope -> @call.lexical_scope +} + +(call receiver:(_)@recv method:(_)@mthd_name)@call { + node dot + attr (dot) push_symbol = "." + + node refine_scope + attr (refine_scope) is_endpoint + edge refine_scope -> @call.lexical_scope + + node refine_in + attr (refine_in) push_scoped_symbol = "", scope = refine_scope + + edge @mthd_name.ref -> dot + edge dot -> refine_in + edge refine_in -> @recv.value +} + +(method name:(_)@name)@method { + node dot + attr (dot) pop_symbol = "." + + node def + attr (def) node_definition = @name + + edge @method.instance_defs -> dot + edge dot -> def +} + +;; refine +(call method:(_)@_mthd_name (#eq? @_mthd_name "refine") arguments:(argument_list (_)@name) block:(do_block body:(_)@body))@call { + node refine_scope + attr (refine_scope) is_endpoint + + node using + attr (using) pop_symbol = "" + + node refine + attr (refine) push_scoped_symbol = "", scope = refine_scope + + edge @call.static_defs -> using + edge using -> refine + edge refine -> @name.ref_value + edge @name.ref -> @call.lexical_scope + + edge refine_scope -> @body.instance_defs +} + +(singleton_method name:(_)@name)@method { + node dot + attr (dot) pop_symbol = "." + + node def + attr (def) node_definition = @name + + edge @method.static_defs -> dot + edge dot -> def +} + +;; using +(call method:(_)@_mthd_name (#eq? @_mthd_name "using") arguments:(argument_list (_)@name))@call { + node pop_using + attr (pop_using) pop_symbol = "" + + node push_using + attr (push_using) push_symbol = "" + + edge @call.local_defs -> pop_using + edge pop_using -> push_using + edge push_using -> @name.ref_value + edge @name.ref -> @call.lexical_scope +} + +;; Identifiers +;; ^^^^^^^^^^^ + +;; Identifiers are single or scoped names and can appear in reference +;; and definition positions. These rules create both reference and +;; definition nodes, and ensure both support arbitrarily deep nesting. +;; +;; The nodes available to connect the refences are: +;; .ref The reference. This node should be connected to the +;; lexical scope where the definition should resolve. +;; .ref_value The value the reference resolves to. References that +;; need to be resolved in this value should connect to +;; this node. +;; +;; The nodes available to connect the definitions are: +;; .def The definition. Scopes exposing the definition should +;; create an edge to this node. +;; .def_value The body of this definition. This node should be connected +;; to the definitions that are members of this definition. + +[ + (constant) + (identifier) + (scope_resolution) +]@identifier { + node @identifier.def_value + node @identifier.local_def + node @identifier.ref + node @identifier.ref_value + node @identifier.root_def + node @identifier.static_def +} + +;;;; References +;;;; `````````` + +(constant)@constant { + node ref + attr (ref) node_reference = @constant + + node scope_sep + attr (scope_sep) push_symbol = "::" + + edge @constant.ref_value -> ref + edge ref -> scope_sep + edge scope_sep -> @constant.ref +} + +(identifier)@identifier { + node ref + attr (ref) node_reference = @identifier + + edge @identifier.ref_value -> ref + edge ref -> @identifier.ref +} + +(scope_resolution scope:(_)@scope name:(_)@name)@scope_res { + edge @scope_res.ref_value -> @name.ref_value + edge @name.ref -> @scope.ref_value + edge @scope.ref -> @scope_res.ref +} + +(scope_resolution !scope name:(_)@name)@scope_res { + node root_scope + attr (root_scope) symbol_reference = "<::>" + + edge @scope_res.ref_value -> @name.ref_value + edge @name.ref -> root_scope + edge root_scope -> @scope_res.ref +} + +;;;; Definitions +;;;; ``````````` + +(constant)@constant { + node def + attr (def) node_definition = @constant + + node scope_sep + attr (scope_sep) pop_symbol = "::" + + edge @constant.static_def -> scope_sep + edge scope_sep -> def + edge def -> @constant.def_value +} + +(identifier)@identifier { + node def + attr (def) node_definition = @identifier + + edge @identifier.local_def -> def + edge def -> @identifier.def_value +} + +(scope_resolution scope:(_)@scope name:(_)@name)@scope_res { + edge @scope_res.root_def -> @scope.root_def + edge @scope_res.static_def -> @scope.static_def + edge @scope.def_value -> @name.static_def + edge @name.def_value -> @scope_res.def_value +} + +(scope_resolution !scope name:(_)@name)@scope_res { + edge @scope_res.root_def -> @name.static_def + edge @name.def_value -> @scope_res.def_value +} + +;; Comments +;; ^^^^^^^^ + +(comment)@comment { + node @comment.def_value + node @comment.instance_defs + node @comment.lexical_scope + node @comment.local_def + node @comment.local_defs + node @comment.ref_value + node @comment.root_def + node @comment.root_defs + node @comment.static_def + node @comment.static_defs +} diff --git a/languages/tree-sitter-stack-graphs-ruby/test/classes/class-definition-scope.rb b/languages/tree-sitter-stack-graphs-ruby/test/classes/class-definition-scope.rb new file mode 100644 index 000000000..76f3dee0f --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/test/classes/class-definition-scope.rb @@ -0,0 +1,12 @@ +class Foo +end + +class Foo::A +end + + Foo::A +# ^ defined: 1,4 +# ^ defined: 4 + + ::Foo +# ^ defined: 1, 4 diff --git a/languages/tree-sitter-stack-graphs-ruby/test/classes/class-definition.rb b/languages/tree-sitter-stack-graphs-ruby/test/classes/class-definition.rb new file mode 100644 index 000000000..d3fdb976d --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/test/classes/class-definition.rb @@ -0,0 +1,5 @@ +class A +end + + A +# ^ defined: 1 diff --git a/languages/tree-sitter-stack-graphs-ruby/test/classes/class-instantiation.rb b/languages/tree-sitter-stack-graphs-ruby/test/classes/class-instantiation.rb new file mode 100644 index 000000000..53bc5e220 --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/test/classes/class-instantiation.rb @@ -0,0 +1,8 @@ +class Foo + def bar + end +end + + Foo.new.bar +# ^ defined: 1 +# ^ defined: 2 diff --git a/languages/tree-sitter-stack-graphs-ruby/test/classes/class-variable.rb b/languages/tree-sitter-stack-graphs-ruby/test/classes/class-variable.rb new file mode 100644 index 000000000..8aa08dd3b --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/test/classes/class-variable.rb @@ -0,0 +1,13 @@ +class A + FOO = 42 +end + +class FOO +end + + A::FOO +# ^ defined: 1 +# ^ defined: 2 + + FOO +# ^ defined: 5 diff --git a/languages/tree-sitter-stack-graphs-ruby/test/classes/nested-class.rb b/languages/tree-sitter-stack-graphs-ruby/test/classes/nested-class.rb new file mode 100644 index 000000000..c6802127d --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/test/classes/nested-class.rb @@ -0,0 +1,10 @@ +class A + class B + FOO = 42 + end +end + + A::B::FOO +# ^ defined: 1 +# ^ defined: 2 +# ^ defined: 3 diff --git a/languages/tree-sitter-stack-graphs-ruby/test/classes/refer-instance-static-methods.rb b/languages/tree-sitter-stack-graphs-ruby/test/classes/refer-instance-static-methods.rb new file mode 100644 index 000000000..a8e97a280 --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/test/classes/refer-instance-static-methods.rb @@ -0,0 +1,14 @@ +class Foo + def bar + end + + def self.bar + end +end + + Foo.new.bar +# ^ defined: 1 + # ^ defined: 2 + + Foo.bar +# ^ defined: 5 diff --git a/languages/tree-sitter-stack-graphs-ruby/test/classes/refer-variable-defined-in-split-class-definition.rb b/languages/tree-sitter-stack-graphs-ruby/test/classes/refer-variable-defined-in-split-class-definition.rb new file mode 100644 index 000000000..b36ef401e --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/test/classes/refer-variable-defined-in-split-class-definition.rb @@ -0,0 +1,16 @@ +class A + FOO = 42 +end + +class A + BAR = FOO + # ^ defined: 2 +end + + A::FOO +# ^ defined: 1, 5 +# ^ defined: 2 + + A::BAR +# ^ defined: 1, 5 +# ^ defined: 6 diff --git a/languages/tree-sitter-stack-graphs-ruby/test/classes/refer-variable-defined-in-split-definition-of-outer-class.rb b/languages/tree-sitter-stack-graphs-ruby/test/classes/refer-variable-defined-in-split-definition-of-outer-class.rb new file mode 100644 index 000000000..9d609a0a1 --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/test/classes/refer-variable-defined-in-split-definition-of-outer-class.rb @@ -0,0 +1,19 @@ +class A + FOO = 42 +end + +class A + class B + BAR = FOO + # ^ defined: 2 + end +end + + A::FOO +# ^ defined: 1, 5 +# ^ defined: 2 + + A::B::BAR +# ^ defined: 1, 5 +# ^ defined: 6 +# ^ defined: 7 diff --git a/languages/tree-sitter-stack-graphs-ruby/test/classes/refine-class.rb b/languages/tree-sitter-stack-graphs-ruby/test/classes/refine-class.rb new file mode 100644 index 000000000..5dc14ba52 --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/test/classes/refine-class.rb @@ -0,0 +1,23 @@ +class C +end + + C.new.foo +# ^ defined: 1 +# ^ defined: + +module M + refine C do + def foo + end + end +end + + C.new.foo +# ^ defined: 1 +# ^ defined: + +using M + + C.new.foo +# ^ defined: 1 +# ^ defined: 10 diff --git a/languages/tree-sitter-stack-graphs-ruby/test/classes/refine-inner-class.rb.skip b/languages/tree-sitter-stack-graphs-ruby/test/classes/refine-inner-class.rb.skip new file mode 100644 index 000000000..541bb682b --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/test/classes/refine-inner-class.rb.skip @@ -0,0 +1,28 @@ +class C +end + +module M + class C + end + + refine C do + # ^ defined: 5 + def foo + end + end +end + +using M + + M::C.new.foo +# ^ defined: 4 +# ^ defined: 5 +# ^ defined: 9 + + C.new.foo +# ^ defined: 1 +# ^ defined: +## This assertion is failing because, even though reference C on +## line 8 resolves to definition C on line 5 by itself, it resolves +## to the definition of C on line 1 when resolving the method. This +## is an example of https://github.com/github/stack-graphs/issues/275. diff --git a/languages/tree-sitter-stack-graphs-ruby/test/classes/root-variable-inside-nested-class.rb b/languages/tree-sitter-stack-graphs-ruby/test/classes/root-variable-inside-nested-class.rb new file mode 100644 index 000000000..26934d604 --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/test/classes/root-variable-inside-nested-class.rb @@ -0,0 +1,13 @@ +class A + class B + ::FOO = 42 + end +end + +::FOO +# ^ defined: 3 + +::A::B::FOO +# ^ defined: 1 +# ^ defined: 2 +# ^ defined: diff --git a/languages/tree-sitter-stack-graphs-ruby/test/classes/split-nested-class-definition.rb b/languages/tree-sitter-stack-graphs-ruby/test/classes/split-nested-class-definition.rb new file mode 100644 index 000000000..b0e0187f0 --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/test/classes/split-nested-class-definition.rb @@ -0,0 +1,21 @@ +class A + class B + FOO = 42 + end +end + +class A + class B + BAR = 11 + end +end + + A::B::FOO +# ^ defined: 1, 7 +# ^ defined: 2, 8 +# ^ defined: 3 + + A::B::BAR +# ^ defined: 1, 7 +# ^ defined: 2, 8 +# ^ defined: 9 diff --git a/languages/tree-sitter-stack-graphs-ruby/test/includes/modules.rb.skip b/languages/tree-sitter-stack-graphs-ruby/test/includes/modules.rb.skip new file mode 100644 index 000000000..30c5e1ca5 --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/test/includes/modules.rb.skip @@ -0,0 +1,75 @@ +# --- path: a_module.rb --- +module A + CONST = 1 + def a; end +end + +# --- path: b_module.rb --- +$LOAD_PATH << '.' +require 'a_module' + +module B + include A + # ^ defined: 1 + + def b; end +end + +include B + # ^ defined: 4 + a +# ^ defined: 3 + + CONST +# ^ defined: 2 + +C = Module.new do + def c; end + def b; end +end + +self.extend(C) + # ^ defined: 19 + +self.c + # ^ defined: 20 + +# --- path: d_class.rb --- +$LOAD_PATH << '.' +require 'b_module' + +class D + include B + # ^ defined: 4 + extend C + # ^ defined: 19 + + def calling_a + puts "calling a: #{a}" + # ^ defined: 3 + end + + def calling_b + puts "calling b: #{b}" + # ^ defined: 8 + end + + def self.calling_b + puts "calling self.b: #{b}" + # ^ defined: 21 + end + + def self.calling_c + puts "calling c: #{c}" + # ^ defined: 20 + end +end + +d = D.new + # ^ defined: 4 +d.c +# ^ defined: 5, 20 +d.calling_c +# ^ defined: 13 +d.b +# ^ defined: 4, 21 diff --git a/languages/tree-sitter-stack-graphs-ruby/test/literals/array.rb.skip b/languages/tree-sitter-stack-graphs-ruby/test/literals/array.rb.skip new file mode 100644 index 000000000..ceeb49b03 --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/test/literals/array.rb.skip @@ -0,0 +1,10 @@ +a = [1, + 2, + 3] +b = a[0] +c = a[1] + + b +# ^ defined: 4, 1 + c +# ^ defined: 5, 1, 2 diff --git a/languages/tree-sitter-stack-graphs-ruby/test/modules/split-module-definition.rb b/languages/tree-sitter-stack-graphs-ruby/test/modules/split-module-definition.rb new file mode 100644 index 000000000..e8eb11628 --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/test/modules/split-module-definition.rb @@ -0,0 +1,16 @@ +module M + FOO = 42 +end + +module M + BAR = FOO + # ^ defined: 2 +end + + M::FOO +# ^ defined: 1, 5 +# ^ defined: 2 + + M::BAR +# ^ defined: 1, 5 +# ^ defined: 6 diff --git a/languages/tree-sitter-stack-graphs-ruby/test/statements/assignments.rb.skip b/languages/tree-sitter-stack-graphs-ruby/test/statements/assignments.rb.skip new file mode 100644 index 000000000..67b4d35e9 --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/test/statements/assignments.rb.skip @@ -0,0 +1,9 @@ +a = 1 + a +# ^ defined: 1 + +b = (a = 2) + b +# ^ defined: 5 + a +# ^ defined: 1, 5 diff --git a/languages/tree-sitter-stack-graphs-ruby/test/statements/local-variables.rb b/languages/tree-sitter-stack-graphs-ruby/test/statements/local-variables.rb new file mode 100644 index 000000000..8a8569985 --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/test/statements/local-variables.rb @@ -0,0 +1,14 @@ +foo = 42 + +module M + bar = foo + # ^ defined: 1 + bar + # ^ defined: 4 +end + +foo +# ^ defined: 1 + +bar +# ^ defined: diff --git a/languages/tree-sitter-stack-graphs-ruby/test/statements/refer-to-local-class-unqualified.rb b/languages/tree-sitter-stack-graphs-ruby/test/statements/refer-to-local-class-unqualified.rb new file mode 100644 index 000000000..844c4c0c2 --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/test/statements/refer-to-local-class-unqualified.rb @@ -0,0 +1,22 @@ +class C +end + +module M + class C + def foo + end + end + + C.new.foo + # ^ defined: 5 + # ^ defined: 6 +end + + M::C.new.foo +# ^ defined: 4 +# ^ defined: 5 +# ^ defined: 6 + + C.new.foo +# ^ defined: 1 +# ^ defined: diff --git a/languages/tree-sitter-stack-graphs-ruby/test/statements/refer-to-local-qualified-class.rb b/languages/tree-sitter-stack-graphs-ruby/test/statements/refer-to-local-qualified-class.rb new file mode 100644 index 000000000..b9a9d99aa --- /dev/null +++ b/languages/tree-sitter-stack-graphs-ruby/test/statements/refer-to-local-qualified-class.rb @@ -0,0 +1,22 @@ +class N::C +end + +module M + class N::C + def foo + end + end + + N::C.new.foo + # ^ defined: 5 + # ^ defined: 6 +end + + M::N::C.new.foo +# ^ defined: 4 +# ^ defined: 5 +# ^ defined: 6 + + N::C.new.foo +# ^ defined: 1 +# ^ defined: diff --git a/languages/tree-sitter-stack-graphs-ruby/test/test.rb b/languages/tree-sitter-stack-graphs-ruby/test/test.rb new file mode 100644 index 000000000..e69de29bb