Repository: uber/piranha (language: python)

Issue:
Piranha-Generic release (pending items)
- [x] Rename `piranha-tree-sitter` to `piranha`
- [x] More reorganization
- [x] Improve CI 
- [x] Add unit tests
- [ ] Add tests for rules ?


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 04da1623e..a243837a1 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -11,11 +11,22 @@ jobs:
     name: Piranha
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
-      - uses: actions-rs/toolchain@v1
+      - name: Checkout Piranha sources
+        uses: actions/checkout@v2
+      - name: Install stable toolchain
+        uses: actions-rs/toolchain@v1
         with:
+          profile: minimal
           toolchain: stable
-      - name: Install-Tree-Sitter
+          override: true
+          components: rustfmt, clippy
+      - name: Install Tree-Sitter
         run: cargo install tree-sitter-cli
+      - name: Build Piranha
+        run: cargo build
+        working-directory: generic/piranha
+      - name: Clippy check
+        run: cargo clippy
+        working-directory: generic/piranha
       - run: cargo test
         working-directory: generic/piranha
\ No newline at end of file
diff --git a/generic/README.md b/generic/README.md
index 30a9ed71a..6380f3892 100644
--- a/generic/README.md
+++ b/generic/README.md
@@ -1,17 +1,17 @@
-# Generic Piranha 
-It scans source files to delete code related to stale feature flags leading to a cleaner, safer, more performant, and more maintainable code base.
-This generic tree-sitter based implementation for Piranha makes it easy to onboard new languages and new feature flag systems.
+# Multilingual Piranha
+This variant of Piranha provides a flexible multilingual solution for deleting code related to stale feature flags leading to a cleaner, safer, more performant, and more maintainable code base.
 
 ## Motivation 
 
-Adding Piranha support for a new language requires re-implementing the entire refactoring logic for that particular language. This is time-consuming and expensive to develop and maintain such nearly similar implementations.
-This implementation overcomes this problem by extracting the language specific syntactic transformations to tree-sitter query API based rewrite rules, and applying them to the input program as chains of rules.
+Adding Piranha support for a new language requires re-implementing the entire refactoring logic for that particular language. It is time-consuming and expensive to develop and maintain these similar implementations.
+This is overcome by extracting the language specific syntactic transformations to tree-sitter query based rewrite rules, and applying them to the input program as chains of rules.
 
 ## Usage 
 Piranha can be configured to recognize different flag APIs by specifying a `rules.toml` file (and optionally a `edges.toml`). Piranha will then perform the refactoring based on the flag behavior, which can be specified by providing `piranha_arguments.toml`. Moreover, Piranha can be configured to operate upon a new language by specifying a `/configuration/<lang-name>/rules.toml`, `/configuration/<lang-name>/edges.toml` and `/configuration/<lang-name>/scope_generators.toml`.
 
 ```
-piranha 0.1.0
+Piranha
+A refactoring tool that eliminates dead code related to stale feature flags.
 
 USAGE:
     piranha --path-to-codebase <PATH_TO_CODEBASE> --path-to-configurations <PATH_TO_CONFIGURATIONS>
@@ -21,7 +21,8 @@ OPTIONS:
             Path to source code folder
 
     -f, --path-to-configurations <PATH_TO_CONFIGURATIONS>
-            Folder containing the required configuration files
+            Directory containing the configuration files - `piranha_arguments.toml`, `rules.toml`,
+            and `edges.toml` (optional)
 
     -h, --help
             Print help information
@@ -33,7 +34,7 @@ OPTIONS:
 Languages supported :
 * Java
 * Kotlin (planned)
-* Java / Kotlin (planned)
+* Java + Kotlin (planned)
 * Swift (planned)
 * JavaScript (planned)
 * Go (requested)
@@ -43,7 +44,7 @@ Languages supported :
 * PHP (requested)
 * Contributions for the `requested` languages or any other languages are welcome :) 
 
-## Obtain Piranha Binary from source
+## Building Piranha from Source
 * Install [Rust](https://www.rust-lang.org/tools/install), Git and [tree-sitter CLI](https://github.com/tree-sitter/tree-sitter/blob/master/cli/README.md)
 * Checkout this repository - `git checkout https://github.com/uber/piranha.git` 
 * `cd piranha/generic/piranha`
@@ -52,25 +53,24 @@ Languages supported :
 
 ## Getting started with Piranha
 
-*Please refer to our demo - `generic/piranha/demo/` to quickly get started with Piranha.*
-*Please refer to our test cases at `src/test-resources/<language>/` as a reference for handling complicated scenarios*
-
+*Please refer to our [demo](/generic/piranha/demo/run_piranha_demo.sh) - to quickly get started with Piranha.*
 To run the demo : 
 * `cd generic/piranha`
 * `./demo/run_piranha_demo.sh`
+*Please refer to our test cases at [`/generic/piranha/src/test-resources/<language>/`](/generic/piranha/src/test-resources/) as a reference for handling complicated scenarios*
 
 To get started with Piranha, please follow the below steps:
 * Check if the current version of Piranha supports the required language.
-* If so, then check if the API usage is similar to the ones provided in the test suite at `/demo/<language>/configurations/rules.toml`.
-*  If not, adapt these examples to your requirements. Look at the [tree-sitter query documentation](https://tree-sitter.github.io/tree-sitter/using-parsers#pattern-matching-with-queries) for more information on how to construct tree-sitter queries. 
-* Now adapt the `/demo/<language>/configurations/piranha_arguments.toml` as per your requirements. For instance, you may want to update the value corresponding to the `@stale_flag_name` and `@treated`. If your rules do not contain require other tags feel free to remove them from your `piranha_arguments.toml`. In most cases, one will not require `/demo/<language>/configurations/edges.toml`.
+* If so, then check if the API usage is similar to the ones shown in the demo ([java-demo](/generic/piranha/demo/java/configurations/rules.toml)) or in the test resources ([java-ff_system1](/piranha/test-resources/java/feature_flag_system_1/control/configurations/rules.toml), [java-ff_system2](/piranha/test-resources/java/feature_flag_system_2/control/configurations/rules.toml)).
+*  If not, adapt these examples to your requirements. Further, you can study the [tree-sitter query documentation](https://tree-sitter.github.io/tree-sitter/using-parsers#pattern-matching-with-queries) to understand how tree-sitter queries work.
+* Now adapt the [argument file](/generic/piranha/demo/java/configurations/piranha_arguments.toml) as per your requirements. For instance, you may want to update the value corresponding to the `@stale_flag_name` and `@treated`. If your rules do not contain require other tags feel free to remove them from your arguments file. In most cases [edges file](/demo/java/configurations/edges.toml) is not required, unless your feature flag system API rules are inter-dependent. 
 
-For more details on how to configure Piranha to a new feature flag API see section [Onboarding a new feature flag API](onboarding-a-new-feature-flag-api).
-For more details on how to configure Piranha to a new language see section [Onboarding a new language flag](onboarding-a-new-language).
+More details for configuring Piranha - [Adding support for a new feature flag system](adding-support-for-a-new-feature-flag-system)
+and [Adding Support for a new language](adding-support-for-a-new-language).
 
-## Onboarding a new feature flag system
+## Adding support for a new feature flag system
 
-To onboard a new feature flag system users will have to specify the `<path-to-configurations>/rules.toml` and `<path-to-configurations>/edges.toml` files. The `rules.toml` will contain rules that identify the usage of a feature flag system API. Defining `edges.toml` is required if your feature flag system API rules are inter-dependent. 
+To onboard a new feature flag system users will have to specify the `<path-to-configurations>/rules.toml` and `<path-to-configurations>/edges.toml` files (look [here](/generic/piranha/src/cleanup_rules/java)). The `rules.toml` will contain rules that identify the usage of a feature flag system API. Defining `edges.toml` is required if your feature flag system API rules are inter-dependent. 
 For instance, you want to delete a method declaration with specific annotations and then update its usages with some boolean value. 
 Please refer to the `test-resources/java` for detailed examples. 
 
@@ -91,7 +91,7 @@ class PiranhaDemo {
     }
 }
 ```
-In the case when STALE_FLAG is treated, we would expect Piranha to refactor the code as shown below (assuming) : 
+In the case when STALE_FLAG is treated, we would expect Piranha to refactor the code as shown below (assuming that `STALE_FLAG` is treated) : 
 ```
 class PiranhaDemo {
 
@@ -149,7 +149,7 @@ This file specifies that, the user wants to perform this refactoring for `java`
 The `substitutions` field captures mapping between the tags and their corresponding concrete values. In this example, we specify that the tag named `stale_flag_name` should be replaced with `STALE_FLAG` and `treated` with `true`.
 
 
-## Onboarding a new language 
+## Adding Support for a new language
 This section describes how to configure Piranha to support a new language. 
 Users who do not intend to onboard a new language can skip this section.
 This section will describe how to encode cleanup rules that are triggered based on the update applied to the flag API usages.
@@ -205,39 +205,39 @@ int foobar(){
 </td>
 </table>
 
-We would first define flag API rules as discussed in the section [Onboarding a new feature flag API](onboarding-a-new-feature-flag-api). Assuming this rule replaces the occurrence of the flag API corresponding to `SOME_STALE_FLAG` with `true`; we would have to define more cleanup rules as follows:
+We would first define flag API rules as discussed in the section [Adding Support for a new language](adding-support-for-a-new-language). Assuming this rule replaces the occurrence of the flag API corresponding to `SOME_STALE_FLAG` with `true`; we would have to define more cleanup rules as follows:
 
-* `R0`: Deletes the enclosing variable declaration (i.e. `x`) (E.g. `cleanup_rules/java/rules.toml: delete_variable_declarations`)
-* `R1`: replace the identifier with the RHS of the deleted variable declaration, within the body of the enclosing method where `R0` was applied i.e. replace `x` with `true` within the method body of `foobar`. (E.g. `cleanup_rules/java/rules.toml: replace_expression_with_boolean_literal`) 
-* `R2`: simplify the boolean expressions, for example replace `true || someCondition()` with `true`, that encloses the node where `R1` was applied. (E.g. `cleanup_rules/java/rules.toml: true_or_something`)
-* `R3`: eliminate the enclosing if statement with a constant condition where `R2` was applied (`if (true) { return 100;}` → `return 100;`). E.g. `cleanup_rules/java/rules.toml:  simplify_if_statement_true, remove_unnecessary_nested_block`
-* `R4`: eliminate unreachable code (`return 0;` in `return 100; return 0;`) in the enclosing block where `R3` was applied.
-(E.g. `cleanup_rules/java/rules.toml:  delete_all_statements_after_return`)
+* `R0`: Deletes the enclosing variable declaration (i.e. `x`) (E.g. [java-rules](/generic/piranha/src/cleanup_rules/java/rules.toml):`delete_variable_declarations`)
+* `R1`: replace the identifier with the RHS of the deleted variable declaration, within the body of the enclosing method where `R0` was applied i.e. replace `x` with `true` within the method body of `foobar`. (E.g. [java-rules](/generic/piranha/src/cleanup_rules/java/rules.toml):`replace_expression_with_boolean_literal`) 
+* `R2`: simplify the boolean expressions, for example replace `true || someCondition()` with `true`, that encloses the node where `R1` was applied. (E.g. [java-rules](/generic/piranha/src/cleanup_rules/java/rules.toml): `true_or_something`)
+* `R3`: eliminate the enclosing if statement with a constant condition where `R2` was applied (`if (true) { return 100;}` → `return 100;`). E.g. [java-rules](/generic/piranha/src/cleanup_rules/java/rules.toml): `simplify_if_statement_true, remove_unnecessary_nested_block`
+* `R4`: eliminate unreachable code (`return 0;` in `return 100; return 0;`) in the enclosing block where `R3` was applied. (E.g. [java-rules](/generic/piranha/src/cleanup_rules/java/rules.toml): `delete_all_statements_after_return`)
 
 The fact that `R2` has to be applied to the enclosing node where `R1` was applied, is expressed by specifying the `edges.toml` file. 
 
-To define how these cleanup rules should be chained, one needs to specify edges (e.g. the `cleanup_rules/java/edges.toml` file) between the groups and (or) individual rules.
-The edges can be labelled as `ANCESTOR`, `METHOD`, `CLASS` or `GLOBAL`. 
-* A `ANCESTOR` edge implies that after Piranha applies the `"from"` rule to update the node `n1` in the AST to node `n2`, Piranha tries to apply `"to"` rules on any ancestor of `"n2"` (e.g. `R1` → `R2`, `R2` → `R3`, `R3` → `R4`)
-* A `METHOD` edge implies that after Piranha applies the `"from"` rule to update the node `n1` in the AST to node `n2`, Piranha tries to apply `"to"` rules within the enclosing method's body. (e.g. `R0` → `R1`)
-* A `CLASS` edge implies that after Piranha applies the `"from"` rule to update the node `n1` in the AST to node `n2`, Piranha tries to apply `"to"` rules within the enclosing class body. (e.g. in-lining a private field)
-* A `GLOBAL` edge implies that after Piranha applies the `"from"` rule to update the node `n1` in the AST to node `n2`, Piranha tries to apply `"to"` rules in the entire code base. (e.g. in-lining a public field).
+To define how these cleanup rules should be chained, one needs to specify edges (e.g. the [java-edges](/generic/piranha/src/cleanup_rules/java/edges.toml) file) between the groups and (or) individual rules.
+The edges can be labelled as `Parent`, `Global` or even much finer scopes like `Method` or `Class` (or let's say `functions` in `go-lang`).
+* A `Parent` edge implies that after Piranha applies the `"from"` rule to update the node `n1` in the AST to node `n2`, Piranha tries to apply `"to"` rules on any ancestor of `"n2"` (e.g. `R1` → `R2`, `R2` → `R3`, `R3` → `R4`)
+* A `Method` edge implies that after Piranha applies the `"from"` rule to update the node `n1` in the AST to node `n2`, Piranha tries to apply `"to"` rules within the enclosing method's body. (e.g. `R0` → `R1`)
+* A `Class` edge implies that after Piranha applies the `"from"` rule to update the node `n1` in the AST to node `n2`, Piranha tries to apply `"to"` rules within the enclosing class body. (e.g. in-lining a private field)
+* A `Global` edge implies that after Piranha applies the `"from"` rule to update the node `n1` in the AST to node `n2`, Piranha tries to apply `"to"` rules in the entire code base. (e.g. in-lining a public field).
 
-One would also have to define how to capture the `METHOD` and `CLASS` scopes for the new language by specifying the `scope_config.toml` file.
-Please refer to `/src/cleanup_rules/java/scope_config.toml`.
+`scope_config.toml` file specifies how to capture these fine-grained scopes like `method`, `function`, `lambda`, `class`.
+First decide, what scopes you need to capture, for instance, in Java we capture "Method" and "Class" scopes. Once, you decide the scopes construct scope query generators similar to [java-scope_config](/generic/piranha/src/cleanup_rules/java/scope_config.toml). Each scope query generator has two parts - (i) `matcher` is a tree-sitter query that matches the AST for the scope, and (ii) `generator` is a tree-sitter query with holes that is instantiated with the code snippets corresponding to tags when `matcher` is matched.
 
 
 ## Contributing
 
 ### Naming conventions for the rules 
 * We name the rules in the format - <verb>_<ast_kind>. E.g., `delete_method_declaration` or `replace_expression with_boolean_literal`
-* We name the dummy rules in the format - `<ast_kind>_cleanup` E.g. `statement_cleanup` or `boolean_literal_cleanup`.
+* We name the dummy rules in the format - `<ast_kind>_cleanup` E.g. `statement_cleanup` or `boolean_literal_cleanup`. Using dummy rules (E.g. [java-rules](/generic/piranha/src/cleanup_rules/java/rules.toml): `boolean_literal_cleanup`) makes it easier and cleaner when specifying the flow between rules.
 
 ### Writing tests
-Currently we only maintain integration tests for the implementation and configurations. 
-These integration run Piranha on the test scenarios in `test-resources/<language>/input` and check if the output is as expected (`test-resources/<language>/expected_treated` and `test-resources/<language>/expected_control`).
+Currently we maintain 
+* Unit tests for the internal functionality can be found under `<models|utilities>/unit_test`.
+* End-to-end tests for the configurations execute  Piranha on the test scenarios in `test-resources/<language>/input` and check if the output is as expected (`test-resources/<language>/expected_treated` and `test-resources/<language>/expected_control`).
 
 To add new scenarios to the existing tests for a given language, you can add them to new file in the `input` directory and then create similarly named files with the expected output in `expected_treated` and `expected_control` directory.
-Note that the `piranha_arguments_treated.toml` and `piranha_arguments_control.toml` files must be also updated accordingly. 
+Update the `piranha_arguments_treated.toml` and `piranha_arguments_control.toml` files too.
 
-To add tests for a new language, please add a new `<language>` folder inside `test-resources/` and populate the `input`, `expected_treated` and `expected_control` directories appropriately.
\ No newline at end of file
+To add tests for a new language, please add a new `<language>` folder inside `test-resources/` and populate the `input`, `expected_treated` and `expected_control` directories appropriately.
diff --git a/generic/piranha/Cargo.toml b/generic/piranha/Cargo.toml
index d60883d8d..1b5baa7c2 100644
--- a/generic/piranha/Cargo.toml
+++ b/generic/piranha/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 authors = ["Ameya Ketkar <ketkara@uber.com>", "Murali Krishna Ramanathan <murali@uber.com>"]
 name = "piranha"
-version = "0.1.0"
+version = "0.1.1"
 edition = "2021"
 include = [
     "build.rs",
diff --git a/generic/piranha/demo/run_piranha_demo.sh b/generic/piranha/demo/run_piranha_demo.sh
index 87d82fbe0..668296e23 100755
--- a/generic/piranha/demo/run_piranha_demo.sh
+++ b/generic/piranha/demo/run_piranha_demo.sh
@@ -10,5 +10,5 @@
 # limitations under the License.
 
 cargo build --release
-cp target/release/piranha  demo/java/piranha
+mv target/release/piranha  demo/java/piranha
 ./demo/java/piranha -c demo/java/ -f demo/java/configurations
\ No newline at end of file
diff --git a/generic/piranha/src/cleanup_rules/java/rules.toml b/generic/piranha/src/cleanup_rules/java/rules.toml
index 15b9849c4..a5f08d4d5 100755
--- a/generic/piranha/src/cleanup_rules/java/rules.toml
+++ b/generic/piranha/src/cleanup_rules/java/rules.toml
@@ -380,7 +380,7 @@ queries = [
 (
 ((assignment_expression
                     left: (_) @a.lhs
-                    right: (_) @a.rhs) @a)
+                    right: (_) @a.rhs) @assignment)
 (#eq? @a.lhs "@variable_name")
 (#not-eq? @a.rhs "@init")
 )"""
@@ -409,7 +409,7 @@ queries = [
   """(
 ((assignment_expression
                     left: (_) @a.lhs
-                    right: (_) @a.rhs) @a)
+                    right: (_) @a.rhs) @assignment)
 (#eq? @a.lhs "@variable_name")
 (#not-eq? @a.rhs "@init")
 )"""
@@ -526,6 +526,10 @@ queries = [
 ]
 
 # Dummy rule that acts as a junction for all boolean based cleanups
+# Let's say you want to define rules from A -> B, A -> C, D -> B, D -> C, ... 
+# A pattern here is - if there is an outgoing edge to B there is another to C.
+# In these cases, you can use a dummy rule X as shown below:
+# X -> B, X - C, A -> X, D -> X, ...
 [[rules]]
 name = "boolean_literal_cleanup"
 query = """
diff --git a/generic/piranha/src/cleanup_rules/java/scope_config.toml b/generic/piranha/src/cleanup_rules/java/scope_config.toml
index 15fcd7fc0..2ee684a84 100755
--- a/generic/piranha/src/cleanup_rules/java/scope_config.toml
+++ b/generic/piranha/src/cleanup_rules/java/scope_config.toml
@@ -13,7 +13,7 @@
 # In `edges.toml`, we specify how the rules should be chained by constructing an edge labelled graph with rules as nodes. 
 # These edges are labelled as - Parent, Global, Method and Class. These edges determine "scope" where the next rule will be applied with respect
 # to the previously applied rule. 
-# Unlike Parent and Global scopes, Method and Class scopes are language dependent, since each language has its own syntax to define methods and classes. 
+# Unlike Parent and Global scopes, Method and Class scopes are language dependent, since each language has its own syntax to define methods, classes, functions. 
 # This file allows users to define these language dependent scopes. 
 # Each scope consists of a scope name and a list of scope rules. 
 # Each scope rule consists of a `matcher` and a `generator`. 
diff --git a/generic/piranha/src/config.rs b/generic/piranha/src/config.rs
index b3ff21bb3..21c6e4032 100644
--- a/generic/piranha/src/config.rs
+++ b/generic/piranha/src/config.rs
@@ -16,12 +16,12 @@ Copyright (c) 2022 Uber Technologies, Inc.
 //! This module defines all basic building block `structs` used by Piranha.
 
 use crate::{
+  models::piranha_arguments::PiranhaArguments,
   models::{
     outgoing_edges::{Edges, OutgoingEdges},
     rule::{Rule, Rules},
     scopes::{ScopeConfig, ScopeGenerator},
   },
-  piranha::piranha_arguments::PiranhaArguments,
   utilities::read_toml,
 };
 
@@ -29,17 +29,16 @@ use std::path::{Path, PathBuf};
 
 use clap::Parser;
 
-/// Used for parsing command-line arguments passed to Piranha .
+/// A refactoring tool that eliminates dead code related to stale feature flags.
 #[derive(Clone, Parser, Debug)]
-#[clap(author, version, about, long_about = None)]
-
-pub struct CommandLineArguments {
+#[clap(name = "Piranha")]
+pub(crate) struct CommandLineArguments {
   /// Path to source code folder.
   #[clap(short = 'c', long)]
-  pub path_to_codebase: String,
-  /// Folder containing the required configuration files
+  pub(crate) path_to_codebase: String,
+  /// Directory containing the configuration files - `piranha_arguments.toml`, `rules.toml`,  and  `edges.toml` (optional).
   #[clap(short = 'f', long)]
-  pub path_to_configurations: String,
+  pub(crate) path_to_configurations: String,
 }
 
 pub(crate) fn read_config_files(
diff --git a/generic/piranha/src/main.rs b/generic/piranha/src/main.rs
index ac2e9e37c..e12f08aea 100644
--- a/generic/piranha/src/main.rs
+++ b/generic/piranha/src/main.rs
@@ -15,8 +15,7 @@ Copyright (c) 2022 Uber Technologies, Inc.
 use std::time::Instant;
 
 use crate::{
-  piranha::{flag_cleaner::FlagCleaner, piranha_arguments::PiranhaArguments},
-  utilities::initialize_logger,
+  models::piranha_arguments::PiranhaArguments, piranha::{execute_piranha}, utilities::initialize_logger,
 };
 use clap::StructOpt;
 use config::CommandLineArguments;
@@ -26,7 +25,7 @@ mod config;
 mod models;
 mod piranha;
 #[cfg(test)]
-mod test;
+mod tests;
 mod utilities;
 
 fn main() {
@@ -35,11 +34,9 @@ fn main() {
 
   let args = PiranhaArguments::new(CommandLineArguments::parse());
 
-  let mut flag_cleaner = FlagCleaner::new(args);
+  let updated_files = execute_piranha(args);
 
-  flag_cleaner.perform_cleanup();
-
-  for source_code_unit in flag_cleaner.get_updated_files() {
+  for source_code_unit in updated_files {
     source_code_unit.persist();
   }
 
diff --git a/generic/piranha/src/models/constraint.rs b/generic/piranha/src/models/constraint.rs
new file mode 100644
index 000000000..9f9d294b6
--- /dev/null
+++ b/generic/piranha/src/models/constraint.rs
@@ -0,0 +1,77 @@
+use std::collections::HashMap;
+
+use serde_derive::Deserialize;
+use tree_sitter::Node;
+
+use crate::utilities::tree_sitter_utilities::{
+  get_node_for_range, substitute_tags, PiranhaHelpers,
+};
+
+use super::{rule_store::RuleStore, source_code_unit::SourceCodeUnit};
+
+#[derive(Deserialize, Debug, Clone, Hash, PartialEq, Eq)]
+pub(crate) struct Constraint {
+  /// Scope in which the constraint query has to be applied
+  matcher: String,
+  /// The Tree-sitter queries that need to be applied in the `matcher` scope
+  queries: Vec<String>,
+}
+
+impl Constraint {
+  pub(crate) fn queries(&self) -> &[String] {
+    &self.queries
+  }
+
+  pub(crate) fn matcher(&self) -> String {
+    String::from(&self.matcher)
+  }
+
+  /// Checks if the node satisfies the constraints.
+  /// Constraint has two parts (i) `constraint.matcher` (ii) `constraint.query`.
+  /// This function traverses the ancestors of the given `node` until `constraint.matcher` matches
+  /// i.e. finds scope for constraint.
+  /// Within this scope it checks if the `constraint.query` DOES NOT MATCH any sub-tree.
+  pub(crate) fn is_satisfied(
+    &self, node: Node, source_code_unit: SourceCodeUnit, rule_store: &mut RuleStore,
+    substitutions: &HashMap<String, String>,
+  ) -> bool {
+    let mut current_node = node;
+    // Get the scope_node of the constraint (`scope.matcher`)
+    while let Some(parent) = current_node.parent() {
+      if let Some((range, _)) = parent.get_match_for_query(
+        &source_code_unit.code(),
+        rule_store.get_query(&self.matcher()),
+        false,
+      ) {
+        let scope_node = get_node_for_range(
+          source_code_unit.root_node(),
+          range.start_byte,
+          range.end_byte,
+        );
+        // Apply each query within the `scope_node`
+        for query_with_holes in self.queries() {
+          let query_str = substitute_tags(query_with_holes.to_string(), substitutions);
+          let query = &rule_store.get_query(&query_str);
+          // If this query matches anywhere within the scope, return false.
+          if scope_node
+            .get_match_for_query(&source_code_unit.code(), query, true)
+            .is_some()
+          {
+            return false;
+          }
+        }
+        break;
+      }
+      current_node = parent;
+    }
+
+    true
+  }
+}
+
+impl Constraint {
+  #[cfg(test)]
+  pub(crate) fn new(matcher: String, queries: Vec<String>) -> Self {
+    Self { matcher, queries }
+  }
+}
diff --git a/generic/piranha/src/models/edit.rs b/generic/piranha/src/models/edit.rs
new file mode 100644
index 000000000..ed325bc72
--- /dev/null
+++ b/generic/piranha/src/models/edit.rs
@@ -0,0 +1,43 @@
+use std::collections::HashMap;
+
+use tree_sitter::Range;
+
+use super::rule::Rule;
+
+pub(crate) struct Edit {
+  replacement_range: Range,
+  replacement_string: String,
+  matched_rule: Rule,
+  matches: HashMap<String, String>,
+}
+
+impl Edit {
+  pub(crate) fn new(
+    replacement_range: Range, replacement_string: String, matched_rule: Rule,
+    matches: HashMap<String, String>,
+  ) -> Self {
+    Self {
+      replacement_range,
+      replacement_string,
+      matched_rule,
+      matches,
+    }
+  }
+
+  /// Get the edit's replacement range.
+  pub(crate) fn replacement_range(&self) -> Range {
+    self.replacement_range
+  }
+
+  pub(crate) fn replacement_string(&self) -> &str {
+    self.replacement_string.as_ref()
+  }
+
+  pub(crate) fn matched_rule(&self) -> Rule {
+    self.matched_rule.clone()
+  }
+
+  pub(crate) fn matches(&self) -> &HashMap<String, String> {
+    &self.matches
+  }
+}
diff --git a/generic/piranha/src/models/mod.rs b/generic/piranha/src/models/mod.rs
index 91eac622e..97c6e333c 100644
--- a/generic/piranha/src/models/mod.rs
+++ b/generic/piranha/src/models/mod.rs
@@ -11,8 +11,13 @@ Copyright (c) 2022 Uber Technologies, Inc.
  limitations under the License.
 */
 
-pub mod outgoing_edges;
-pub mod piranha_config;
-pub mod rule;
-pub mod rule_graph;
-pub mod scopes;
+pub(crate) mod constraint;
+pub(crate) mod edit;
+pub(crate) mod outgoing_edges;
+pub(crate) mod piranha_arguments;
+pub(crate) mod piranha_config;
+pub(crate) mod rule;
+pub(crate) mod rule_graph;
+pub(crate) mod rule_store;
+pub(crate) mod scopes;
+pub(crate) mod source_code_unit;
diff --git a/generic/piranha/src/models/outgoing_edges.rs b/generic/piranha/src/models/outgoing_edges.rs
index 73cae8ea3..2fc85d55f 100644
--- a/generic/piranha/src/models/outgoing_edges.rs
+++ b/generic/piranha/src/models/outgoing_edges.rs
@@ -16,7 +16,7 @@ use serde_derive::Deserialize;
 #[derive(Deserialize, Debug, Clone, Hash, PartialEq, Eq, Default)]
 // Represents the `edges.toml` file
 pub(crate) struct Edges {
-  pub edges: Vec<OutgoingEdges>,
+  pub(crate) edges: Vec<OutgoingEdges>,
 }
 
 // Captures an entry from the `edges.toml` file.
@@ -28,20 +28,14 @@ pub(crate) struct OutgoingEdges {
 }
 
 impl OutgoingEdges {
-  /// Get a reference to the edge's from.
-  #[must_use]
-  pub(crate) fn from_rule(&self) -> String {
+  pub(crate) fn source(&self) -> String {
     String::from(&self.from)
   }
 
-  /// Get a reference to the edge's to.
-  #[must_use]
-  pub(crate) fn to_rules(&self) -> Vec<String> {
+  pub(crate) fn sinks(&self) -> Vec<String> {
     self.to.clone()
   }
 
-  /// Get a reference to the edge's scope.
-  #[must_use]
   pub(crate) fn scope(&self) -> &str {
     self.scope.as_ref()
   }
diff --git a/generic/piranha/src/piranha/piranha_arguments.rs b/generic/piranha/src/models/piranha_arguments.rs
similarity index 79%
rename from generic/piranha/src/piranha/piranha_arguments.rs
rename to generic/piranha/src/models/piranha_arguments.rs
index eba3d49c9..4b6c0609a 100644
--- a/generic/piranha/src/piranha/piranha_arguments.rs
+++ b/generic/piranha/src/models/piranha_arguments.rs
@@ -19,13 +19,13 @@ use tree_sitter::Language;
 
 use crate::{
   config::CommandLineArguments,
-  models::piranha_config::PiranhaConfig,
+  models::piranha_config::PiranhaConfiguration,
   utilities::{read_toml, tree_sitter_utilities::TreeSitterHelpers},
 };
 
 #[derive(Clone)]
-/// Captures the processed Piranha arguments (PiranhaArgsFromConfig) that are parsed from `path_to_feature_flag_rules`.
-pub struct PiranhaArguments {
+/// Captures the processed Piranha arguments (Piranha-Configuration) parsed from `path_to_feature_flag_rules`.
+pub(crate) struct PiranhaArguments {
   /// Path to source code folder.
   path_to_code_base: String,
   // Input arguments provided to Piranha, mapped to tag names -
@@ -45,7 +45,8 @@ impl PiranhaArguments {
     let path_to_piranha_argument_file =
       PathBuf::from(args.path_to_configurations.as_str()).join("piranha_arguments.toml");
 
-    let piranha_args_from_config: PiranhaConfig = read_toml(&path_to_piranha_argument_file, false);
+    let piranha_args_from_config: PiranhaConfiguration =
+      read_toml(&path_to_piranha_argument_file, false);
 
     let input_substitutions = piranha_args_from_config.substitutions();
 
@@ -81,3 +82,17 @@ impl PiranhaArguments {
     self.language_name.as_ref()
   }
 }
+
+#[cfg(test)]
+impl PiranhaArguments {
+  pub(crate) fn dummy() -> Self {
+    let language_name = String::from("java");
+    PiranhaArguments {
+      path_to_code_base: String::new(),
+      input_substitutions: HashMap::new(),
+      path_to_configurations: String::new(),
+      language: language_name.get_language(),
+      language_name,
+    }
+  }
+}
diff --git a/generic/piranha/src/models/piranha_config.rs b/generic/piranha/src/models/piranha_config.rs
index c466d1cf2..9083fc024 100644
--- a/generic/piranha/src/models/piranha_config.rs
+++ b/generic/piranha/src/models/piranha_config.rs
@@ -17,12 +17,12 @@ use serde_derive::Deserialize;
 
 /// Captures the Piranha arguments by from the file at `path_to_feature_flag_rules`.
 #[derive(Deserialize, Debug, Clone, Hash, PartialEq, Eq, Default)]
-pub(crate) struct PiranhaConfig {
+pub(crate) struct PiranhaConfiguration {
   language: Vec<String>,
   substitutions: Vec<Vec<String>>,
 }
 
-impl PiranhaConfig {
+impl PiranhaConfiguration {
   pub(crate) fn substitutions(&self) -> HashMap<String, String> {
     self
       .substitutions
diff --git a/generic/piranha/src/models/rule.rs b/generic/piranha/src/models/rule.rs
index 5dafcc961..2079d1c6c 100644
--- a/generic/piranha/src/models/rule.rs
+++ b/generic/piranha/src/models/rule.rs
@@ -15,14 +15,22 @@ use std::collections::HashMap;
 
 use colored::Colorize;
 use serde_derive::Deserialize;
+use tree_sitter::Node;
 
-use crate::utilities::MapOfVec;
+use crate::utilities::{
+  tree_sitter_utilities::{get_context, get_node_for_range, substitute_tags, PiranhaHelpers},
+  MapOfVec,
+};
+
+use super::{
+  constraint::Constraint, edit::Edit, rule_store::RuleStore, source_code_unit::SourceCodeUnit,
+};
 
 static FEATURE_FLAG_API_GROUP: &str = "Feature-flag API cleanup";
 
 #[derive(Deserialize, Debug, Clone, Hash, PartialEq, Eq, Default)]
 // Represents the `rules.toml` file
-pub struct Rules {
+pub(crate) struct Rules {
   pub(crate) rules: Vec<Rule>,
 }
 
@@ -47,23 +55,6 @@ pub(crate) struct Rule {
 }
 
 impl Rule {
-  /// Create a new query from `self` with the input `query` and `replace`
-  pub(crate) fn update(
-    &self, query: String, replace: String, substitutions: &HashMap<String, String>,
-  ) -> Result<Rule, String> {
-    if substitutions.len() != self.holes().len() {
-      #[rustfmt::skip]
-        return Err(format!("Could not instantiate a rule - {:?}. Some Holes {:?} not found in table {:?}", self, self.holes(), substitutions));
-    } else {
-      let mut updated_rule = self.clone();
-      if !updated_rule.holes().is_empty() {
-        updated_rule.set_query(query.substitute_tags(substitutions));
-        updated_rule.set_replace(replace.substitute_tags(substitutions));
-      }
-      Ok(updated_rule)
-    }
-  }
-
   pub(crate) fn is_feature_flag_cleanup(&self) -> bool {
     self.groups().iter().any(|t| t.eq(FEATURE_FLAG_API_GROUP))
   }
@@ -82,9 +73,38 @@ impl Rule {
       panic!("{}", format!("Could not instantiate the rule {:?} with substitutions {:?}", self, substitutions).red());
   }
 
+  /// Tries to instantiate the rule (`self`) based on the substitutions.
+  /// Note this could fail if the `substitutions` doesn't contain mappings for each hole.
+  pub(crate) fn try_instantiate(
+    &self, substitutions: &HashMap<String, String>,
+  ) -> Result<Rule, String> {
+    let relevant_substitutions = self
+      .holes()
+      .iter()
+      .filter_map(|hole| substitutions.get(hole).map(|subs| (hole, subs)))
+      .map(|(a, b)| (a.clone(), b.clone()))
+      .collect::<HashMap<String, String>>();
+    self.update(&relevant_substitutions)
+  }
+
+  /// Create a new query from `self` by updating the `query` and `replace` based on the substitutions.
+  fn update(&self, substitutions: &HashMap<String, String>) -> Result<Rule, String> {
+    if substitutions.len() != self.holes().len() {
+      #[rustfmt::skip]
+        return Err(format!("Could not instantiate a rule - {:?}. Some Holes {:?} not found in table {:?}", self, self.holes(), substitutions));
+    } else {
+      let mut updated_rule = self.clone();
+      if !updated_rule.holes().is_empty() {
+        updated_rule.update_query(substitutions);
+        updated_rule.update_replace(substitutions);
+      }
+      Ok(updated_rule)
+    }
+  }
+
   /// Groups the rules based on the field `rule.groups`
   /// Note: a rule can belong to more than one group.
-  pub(crate) fn get_grouped_rules(
+  pub(crate) fn group_rules(
     rules: &Vec<Rule>,
   ) -> (HashMap<String, Rule>, HashMap<String, Vec<String>>) {
     let mut rules_by_name = HashMap::new();
@@ -98,20 +118,6 @@ impl Rule {
     (rules_by_name, rules_by_group)
   }
 
-  /// Tries to instantiate the rule (`self`) based on the substitutions.
-  /// Note this could fail if the `substitutions` doesn't contain mappings for each hole.
-  pub(crate) fn try_instantiate(
-    &self, substitutions: &HashMap<String, String>,
-  ) -> Result<Rule, String> {
-    let relevant_substitutions = self
-      .holes()
-      .iter()
-      .filter_map(|hole| substitutions.get(hole).map(|subs| (hole, subs)))
-      .map(|(a, b)| (a.clone(), b.clone()))
-      .collect::<HashMap<String, String>>();
-    self.update(self.query(), self.replace(), &relevant_substitutions)
-  }
-
   /// Records the string that should be grepped in order to find files that
   /// potentially could match this global rule.
   pub(crate) fn add_grep_heuristics_for_global_rules(
@@ -179,50 +185,105 @@ impl Rule {
     }
   }
 
-  pub(crate) fn set_replace(&mut self, replace: String) {
-    self.replace = replace;
+  pub(crate) fn update_replace(&mut self, substitutions: &HashMap<String, String>) {
+    self.replace = substitute_tags(self.replace(), substitutions);
   }
 
-  pub(crate) fn set_query(&mut self, query: String) {
-    self.query = query;
+  pub(crate) fn update_query(&mut self, substitutions: &HashMap<String, String>) {
+    self.query = substitute_tags(self.query(), substitutions);
   }
 
   pub(crate) fn name(&self) -> String {
     String::from(&self.name)
   }
-}
-
-#[derive(Deserialize, Debug, Clone, Hash, PartialEq, Eq)]
-pub struct Constraint {
-  /// Scope in which the constraint query has to be applied
-  matcher: String,
-  /// The Tree-sitter queries that need to be applied in the `matcher` scope
-  queries: Vec<String>,
-}
 
-impl Constraint {
-  pub(crate) fn queries(&self) -> &[String] {
-    &self.queries
+  // Apply all the `rules` to the node, parent, grand parent and great grand parent.
+  // Short-circuit on the first match.
+  pub(crate) fn get_edit_for_context(
+    source_code_unit: &SourceCodeUnit, previous_edit_start: usize, previous_edit_end: usize,
+    rules_store: &mut RuleStore, rules: &Vec<Rule>,
+  ) -> Option<Edit> {
+    // Context contains -  the changed node in the previous edit, its's parent, grand parent and great grand parent
+    let context = || {
+      get_context(
+        source_code_unit.root_node(),
+        previous_edit_start,
+        previous_edit_end,
+      )
+    };
+    for rule in rules {
+      for ancestor in &context() {
+        if let Some(edit) = rule.get_edit(&source_code_unit.clone(), rules_store, *ancestor, false)
+        {
+          return Some(edit);
+        }
+      }
+    }
+    None
   }
 
-  pub(crate) fn matcher(&self) -> String {
-    String::from(&self.matcher)
+  /// Gets the first match for the rule in `self`
+  pub(crate) fn get_edit(
+    &self, source_code_unit: &SourceCodeUnit, rule_store: &mut RuleStore, node: Node,
+    recursive: bool,
+  ) -> Option<Edit> {
+    // Get all matches for the query in the given scope `node`.
+    let all_query_matches = node.get_all_matches_for_query(
+      source_code_unit.code(),
+      rule_store.get_query(&self.get_query()),
+      recursive,
+      Some(self.replace_node()),
+    );
+
+    // Return the first match that satisfies constraint of the rule
+    for (range, tag_substitutions) in all_query_matches {
+      let matched_node = get_node_for_range(
+        source_code_unit.root_node(),
+        range.start_byte,
+        range.end_byte,
+      );
+      if matched_node.satisfies_constraint(
+        source_code_unit.clone(),
+        self,
+        &tag_substitutions,
+        rule_store,
+      ) {
+        let replacement = substitute_tags(self.replace(), &tag_substitutions);
+        return Some(Edit::new(
+          range,
+          replacement,
+          self.clone(),
+          tag_substitutions,
+        ));
+      }
+    }
+    None
   }
 }
 
-pub trait RuleHelper {
-  /// replaces the all the occurrences of keys (of `substitutions` map) in the string with its corresponding value.
-  fn substitute_tags(&self, substitutions: &HashMap<String, String>) -> String;
-}
-
-impl RuleHelper for String {
-  fn substitute_tags(&self, substitutions: &HashMap<String, String>) -> String {
-    let mut output = String::from(self);
-    for (tag, substitute) in substitutions {
-      // Before replacing the key, it is transformed to a tree-sitter tag by adding `@` as prefix
-      let key = format!("@{}", tag);
-      output = output.replace(&key, substitute)
+#[cfg(test)]
+impl Rule {
+  pub(crate) fn new(
+    name: &str, query: &str, replace_node: &str, replace: &str, holes: Option<Vec<String>>,
+    constraints: Option<Vec<Constraint>>,
+  ) -> Self {
+    Self {
+      name: name.to_string(),
+      query: query.to_string(),
+      replace_node: replace_node.to_string(),
+      replace: replace.to_string(),
+      groups: None,
+      holes,
+      constraints,
+      grep_heuristics: None,
     }
-    output
+  }
+
+  pub(crate) fn dummy() -> Rule {
+    Rule::new("", "", "", "", None, None)
   }
 }
+
+#[cfg(test)]
+#[path = "unit_tests/rule_test.rs"]
+mod rule_test;
diff --git a/generic/piranha/src/models/rule_graph.rs b/generic/piranha/src/models/rule_graph.rs
index 5254b4bc7..d42ae70c3 100644
--- a/generic/piranha/src/models/rule_graph.rs
+++ b/generic/piranha/src/models/rule_graph.rs
@@ -17,12 +17,12 @@ use crate::{
 };
 use std::collections::HashMap;
 
-pub struct RuleGraph(HashMap<String, Vec<(String, String)>>);
+pub(crate) struct RuleGraph(HashMap<String, Vec<(String, String)>>);
 
 impl RuleGraph {
   // Constructs a graph of rules based on the input `edges` that represent the relationship between two rules or groups of rules.
   pub(crate) fn new(edges: &Vec<OutgoingEdges>, all_rules: &Vec<Rule>) -> Self {
-    let (rules_by_name, rules_by_group) = Rule::get_grouped_rules(all_rules);
+    let (rules_by_name, rules_by_group) = Rule::group_rules(all_rules);
 
     // A closure that gets the rules corresponding to the given rule name or group name.
     let get_rules_for_tag_or_name = |val: &String| {
@@ -36,8 +36,8 @@ impl RuleGraph {
     // Add the edge(s) to the graph. Multiple edges will be added
     // when either edge endpoint is a group name.
     for edge in edges {
-      for from_rule in get_rules_for_tag_or_name(&edge.from_rule()) {
-        for outgoing_edge in edge.to_rules() {
+      for from_rule in get_rules_for_tag_or_name(&edge.source()) {
+        for outgoing_edge in edge.sinks() {
           for to_rule in get_rules_for_tag_or_name(&outgoing_edge) {
             // Add edge to the adjacency list
             graph.collect(
@@ -53,8 +53,13 @@ impl RuleGraph {
 
   /// Get all the outgoing edges for `rule_name`
   pub(crate) fn get_neighbors(&self, rule_name: &String) -> Vec<(String, String)> {
-    self
-      .0
-      .get(rule_name).cloned().unwrap_or_default()
+    self.0.get(rule_name).cloned().unwrap_or_default()
+  }
+}
+
+#[cfg(test)]
+impl RuleGraph {
+  pub(crate) fn dummy() -> Self {
+    RuleGraph(HashMap::new())
   }
 }
diff --git a/generic/piranha/src/piranha/rule_store.rs b/generic/piranha/src/models/rule_store.rs
similarity index 84%
rename from generic/piranha/src/piranha/rule_store.rs
rename to generic/piranha/src/models/rule_store.rs
index 1e1bfb3e2..70fad4b33 100644
--- a/generic/piranha/src/piranha/rule_store.rs
+++ b/generic/piranha/src/models/rule_store.rs
@@ -19,21 +19,19 @@ use tree_sitter::{Language, Query};
 
 use crate::{
   config::read_config_files,
+  models::piranha_arguments::PiranhaArguments,
   models::{
     rule::Rule,
     rule_graph::RuleGraph,
     scopes::{ScopeGenerator, ScopeQueryGenerator},
   },
-  piranha::piranha_arguments::PiranhaArguments,
   utilities::{tree_sitter_utilities::TreeSitterHelpers, MapOfVec},
 };
 
-pub static GLOBAL: &str = "Global";
-pub static METHOD: &str = "Method";
-pub static CLASS: &str = "Class";
-pub static PARENT: &str = "Parent";
+pub(crate) static GLOBAL: &str = "Global";
+pub(crate) static PARENT: &str = "Parent";
 /// This maintains the state for Piranha.
-pub struct RuleStore {
+pub(crate) struct RuleStore {
   // A graph that captures the flow amongst the rules
   rule_graph: RuleGraph,
   // Caches the compiled tree-sitter queries.
@@ -92,7 +90,7 @@ impl RuleStore {
     if let Ok(mut r) = rule.try_instantiate(tag_captures) {
       r.add_grep_heuristics_for_global_rules(tag_captures);
       #[rustfmt::skip]
-        info!("{}", format!("Added Global Rule : {:?} - {}", r.name(), r.get_query()).bright_blue());
+      info!("{}", format!("Added Global Rule : {:?} - {}", r.name(), r.get_query()).bright_blue());
       self.global_rules.push(r);
     }
   }
@@ -134,7 +132,7 @@ impl RuleStore {
       }
     }
     // Add empty entry, incase no next rule was found for a particular scope
-    for scope in [PARENT, METHOD, CLASS, GLOBAL] {
+    for scope in [PARENT, GLOBAL] {
       next_rules.entry(scope.to_string()).or_default();
     }
     next_rules
@@ -150,3 +148,28 @@ impl RuleStore {
       .unwrap_or_else(Vec::new)
   }
 }
+
+#[cfg(test)]
+impl RuleStore {
+  pub(crate) fn dummy() -> RuleStore {
+    RuleStore {
+      rule_graph: RuleGraph::dummy(),
+      rule_query_cache: HashMap::new(),
+      rules_by_name: HashMap::new(),
+      global_rules: vec![],
+      piranha_args: PiranhaArguments::dummy(),
+      scopes: vec![],
+    }
+  }
+
+  pub(crate) fn dummy_with_scope(scopes: Vec<ScopeGenerator>) -> RuleStore {
+    RuleStore {
+      rule_graph: RuleGraph::dummy(),
+      rule_query_cache: HashMap::new(),
+      rules_by_name: HashMap::new(),
+      global_rules: vec![],
+      piranha_args: PiranhaArguments::dummy(),
+      scopes,
+    }
+  }
+}
diff --git a/generic/piranha/src/models/scopes.rs b/generic/piranha/src/models/scopes.rs
index 270fed2f4..4f766932f 100644
--- a/generic/piranha/src/models/scopes.rs
+++ b/generic/piranha/src/models/scopes.rs
@@ -14,6 +14,12 @@ Copyright (c) 2022 Uber Technologies, Inc.
 use itertools::Itertools;
 use serde_derive::Deserialize;
 
+use crate::utilities::tree_sitter_utilities::{
+  get_node_for_range, substitute_tags, PiranhaHelpers,
+};
+
+use super::{rule_store::RuleStore, source_code_unit::SourceCodeUnit};
+
 // Represents the content in the `scope_config.toml` file
 #[derive(Deserialize, Debug, Clone, Hash, PartialEq, Eq, Default)]
 pub(crate) struct ScopeConfig {
@@ -43,10 +49,41 @@ impl ScopeGenerator {
   pub(crate) fn rules(&self) -> Vec<ScopeQueryGenerator> {
     self.rules.iter().cloned().collect_vec()
   }
+
+  /// Generate a tree-sitter based query representing the scope of the previous edit.
+  /// We generate these scope queries by matching the rules provided in `<lang>_scopes.toml`.
+  pub(crate) fn get_scope_query(
+    source_code_unit: SourceCodeUnit, scope_level: &str, start_byte: usize, end_byte: usize,
+    rules_store: &mut RuleStore,
+  ) -> String {
+    let root_node = source_code_unit.root_node();
+    let mut changed_node = get_node_for_range(root_node, start_byte, end_byte);
+
+    // Get the scope matchers for `scope_level` from the `scope_config.toml`.
+    let scope_matchers = rules_store.get_scope_query_generators(scope_level);
+
+    // Match the `scope_matcher.matcher` to the parent
+    while let Some(parent) = changed_node.parent() {
+      for m in &scope_matchers {
+        if let Some((_, captures_by_tag)) = parent.get_match_for_query(
+          &source_code_unit.code(),
+          rules_store.get_query(&m.matcher()),
+          false,
+        ) {
+          // Generate the scope query for the specific context by substituting the
+          // the tags with code snippets appropriately in the `generator` query.
+          return substitute_tags(m.generator(), &captures_by_tag);
+        } else {
+          changed_node = parent;
+        }
+      }
+    }
+    panic!("Could not create scope query for {:?}", scope_level);
+  }
 }
 
 #[derive(Deserialize, Debug, Clone, Hash, PartialEq, Eq, Default)]
-pub struct ScopeQueryGenerator {
+pub(crate) struct ScopeQueryGenerator {
   matcher: String, // a tree-sitter query matching some enclosing AST pattern (like method or class)
   generator: String, // a tree-sitter query matching the exact AST node
 }
@@ -60,3 +97,26 @@ impl ScopeQueryGenerator {
     String::from(&self.generator)
   }
 }
+
+#[cfg(test)]
+impl ScopeQueryGenerator {
+  fn new(matcher: &str, generator: &str) -> ScopeQueryGenerator {
+    ScopeQueryGenerator {
+      matcher: matcher.to_string(),
+      generator: generator.to_string(),
+    }
+  }
+}
+#[cfg(test)]
+impl ScopeGenerator {
+  fn new(name: &str, rules: Vec<ScopeQueryGenerator>) -> ScopeGenerator {
+    ScopeGenerator {
+      name: name.to_string(),
+      rules,
+    }
+  }
+}
+
+#[cfg(test)]
+#[path = "unit_tests/scopes_test.rs"]
+mod scopes_test;
diff --git a/generic/piranha/src/models/source_code_unit.rs b/generic/piranha/src/models/source_code_unit.rs
new file mode 100644
index 000000000..01bae8904
--- /dev/null
+++ b/generic/piranha/src/models/source_code_unit.rs
@@ -0,0 +1,97 @@
+use std::{
+  collections::HashMap,
+  fs,
+  path::{Path, PathBuf},
+};
+
+use tree_sitter::{InputEdit, Node, Parser, Tree};
+
+use crate::utilities::tree_sitter_utilities::get_tree_sitter_edit;
+
+use super::edit::Edit;
+
+// Maintains the updated source code content and AST of the file
+#[derive(Clone)]
+pub struct SourceCodeUnit {
+  // The tree representing the file
+  ast: Tree,
+  // The content of a file
+  code: String,
+  // The tag substitution cache.
+  // This map is looked up to instantiate new rules.
+  substitutions: HashMap<String, String>,
+  // The path to the source code.
+  path: PathBuf,
+}
+
+impl SourceCodeUnit {
+  pub(crate) fn new(
+    parser: &mut Parser, code: String, substitutions: &HashMap<String, String>, path: &Path,
+  ) -> Self {
+    let ast = parser.parse(&code, None).expect("Could not parse code");
+    Self {
+      ast,
+      code,
+      substitutions: substitutions.clone(),
+      path: path.to_path_buf(),
+    }
+  }
+
+  pub fn root_node(&self) -> Node<'_> {
+    self.ast.root_node()
+  }
+
+  /// Writes the current contents of `code` to the file system.
+  pub fn persist(&self) {
+    fs::write(&self.path, self.code.as_str()).expect("Unable to Write file");
+  }
+
+  /// Applies an edit to the source code unit
+  /// # Arguments
+  /// * `replace_range` - the range of code to be replaced
+  /// * `replacement_str` - the replacement string
+  /// * `parser`
+  ///
+  /// # Returns
+  /// The `edit:InputEdit` performed.
+  ///
+  /// Note - Causes side effect. - Updates `self.ast` and `self.code`
+  pub(crate) fn apply_edit(&mut self, edit: &Edit, parser: &mut Parser) -> InputEdit {
+    // Get the tree_sitter's input edit representation
+    let (new_source_code, ts_edit) = get_tree_sitter_edit(
+      self.code.clone(),
+      edit.replacement_range(),
+      edit.replacement_string(),
+    );
+    // Apply edit to the tree
+    self.ast.edit(&ts_edit);
+    // Create a new updated tree from the previous tree
+    let new_tree = parser
+      .parse(&new_source_code, Some(&self.ast))
+      .expect("Could not generate new tree!");
+    self.ast = new_tree;
+    self.code = new_source_code;
+    ts_edit
+  }
+
+  // #[cfg(test)] // Rust analyzer FP
+  pub fn code(&self) -> String {
+    String::from(&self.code)
+  }
+  #[cfg(test)] // Rust analyzer FP
+  pub fn path(&self) -> &PathBuf {
+    &self.path
+  }
+
+  pub(crate) fn substitutions(&self) -> &HashMap<String, String> {
+    &self.substitutions
+  }
+
+  pub(crate) fn add_to_substitutions(&mut self, new_entries: &HashMap<String, String>) {
+    let _ = &self.substitutions.extend(new_entries.clone());
+  }
+}
+
+#[cfg(test)]
+#[path = "unit_tests/source_code_unit_test.rs"]
+mod source_code_unit_test;
diff --git a/generic/piranha/src/piranha.rs b/generic/piranha/src/piranha.rs
new file mode 100644
index 000000000..689302af5
--- /dev/null
+++ b/generic/piranha/src/piranha.rs
@@ -0,0 +1,314 @@
+/*
+Copyright (c) 2022 Uber Technologies, Inc.
+
+ <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ except in compliance with the License. You may obtain a copy of the License at
+ <p>http://www.apache.org/licenses/LICENSE-2.0
+
+ <p>Unless required by applicable law or agreed to in writing, software distributed under the
+ License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ express or implied. See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+use std::{collections::HashMap, path::PathBuf};
+
+use colored::Colorize;
+use itertools::Itertools;
+use jwalk::WalkDir;
+use log::info;
+use regex::Regex;
+use tree_sitter::Parser;
+
+use crate::{
+  models::{
+    piranha_arguments::PiranhaArguments, rule_store::RuleStore, source_code_unit::SourceCodeUnit,
+  },
+  utilities::read_file,
+};
+
+use crate::models::scopes::ScopeGenerator;
+use crate::utilities::tree_sitter_utilities::PiranhaHelpers;
+use crate::{
+  models::{
+    rule::Rule,
+    rule_store::{GLOBAL, PARENT},
+  },
+  utilities::tree_sitter_utilities::get_node_for_range,
+};
+
+use std::collections::VecDeque;
+use tree_sitter::{InputEdit, Node};
+
+pub(crate) fn execute_piranha(args: PiranhaArguments) -> Vec<SourceCodeUnit> {
+  let mut flag_cleaner = FlagCleaner::new(args);
+  flag_cleaner.perform_cleanup();
+  flag_cleaner.get_updated_files()
+}
+
+impl SourceCodeUnit {
+  /// Will apply the `rule` to all of its occurrences in the source code unit.
+  fn apply_rule(
+    &mut self, rule: Rule, rules_store: &mut RuleStore, parser: &mut Parser,
+    scope_query: &Option<String>,
+  ) {
+    loop {
+      if !self._apply_rule(rule.clone(), rules_store, parser, scope_query) {
+        break;
+      }
+    }
+  }
+
+  // Applies the rule to the first match in the source code
+  // This is implements the main algorithm of piranha.
+  fn _apply_rule(
+    &mut self, rule: Rule, rules_store: &mut RuleStore, parser: &mut Parser,
+    scope_query: &Option<String>,
+  ) -> bool {
+    let scope_node = self.get_scope_node(scope_query, rules_store);
+
+    let mut any_match = false;
+
+    // Match the rule "anywhere" inside the scope_node
+    if let Some(edit_1) = rule.get_edit(&self.clone(), rules_store, scope_node, true) {
+      any_match = true;
+      // Apply edit_1 
+      let applied_ts_edit = self.apply_edit(&edit_1, parser);
+
+      // Add all the (code_snippet, tag) mapping to the substitution table.
+      self.add_to_substitutions(edit_1.matches());
+
+      let mut current_edit = applied_ts_edit;
+      let mut current_rule = rule.clone();
+      let mut next_rules_stack: VecDeque<(String, Rule)> = VecDeque::new();
+
+      // Perform the parent edits, while queueing the Method and Class level edits.
+      // let file_level_scope_names = [METHOD, CLASS];
+      loop {
+        // Get all the (next) rules that could be after applying the current rule (`rule`).
+        let next_rules_by_scope = rules_store.get_next(&current_rule, self.substitutions());
+
+        // Adds "Method" and "Class" rules to the stack
+        self.add_rules_to_stack(
+          &next_rules_by_scope,
+          current_edit,
+          rules_store,
+          &mut next_rules_stack,
+        );
+
+        // Add Global rules as seed rules
+        for r in &next_rules_by_scope[GLOBAL] {
+          rules_store.add_to_global_rules(r, self.substitutions());
+        }
+
+        // Process the parent
+        // Find the rules to be applied in the "Parent" scope that match any parent (context) of the changed node in the previous edit
+        if let Some(edit) = Rule::get_edit_for_context(
+          &self.clone(),
+          current_edit.start_byte,
+          current_edit.new_end_byte,
+          rules_store,
+          &next_rules_by_scope[PARENT],
+        ) {
+          #[rustfmt::skip]
+          info!( "{}", format!( "Cleaning up the context, by applying the rule - {}", edit.matched_rule().name()).green());
+          // Apply the matched rule to the parent
+          current_edit = self.apply_edit(&edit, parser);
+          current_rule = edit.matched_rule();
+          // Add the (tag, code_snippet) mapping to substitution table.
+          self.add_to_substitutions(edit.matches());
+        } else {
+          // No more parents found for cleanup
+          break;
+        }
+      }
+
+      // Apply the next rules from the stack
+      for (sq, rle) in next_rules_stack {
+        self.apply_rule(rle, rules_store, parser, &Some(sq.to_string()));
+      }
+    }
+    any_match
+  }
+
+  /// Adds the "Method" and "Class" scoped next rules to the queue.
+  fn add_rules_to_stack(
+    &mut self, next_rules_by_scope: &HashMap<String, Vec<Rule>>, current_edit: InputEdit,
+    rules_store: &mut RuleStore, stack: &mut VecDeque<(String, Rule)>,
+  ) {
+    for (scope_level, rules) in next_rules_by_scope {
+      // Scope level is not "PArent" or "Global"
+      if ![PARENT, GLOBAL].contains(&scope_level.as_str()) {
+        for rule in rules {
+          let scope_query = ScopeGenerator::get_scope_query(
+            self.clone(),
+            scope_level,
+            current_edit.start_byte,
+            current_edit.new_end_byte,
+            rules_store,
+          );
+          // Add Method and Class scoped rules to the queue
+          stack.push_front((scope_query, rule.instantiate(self.substitutions())));
+        }
+      }
+    }
+  }
+
+  fn get_scope_node(&self, scope_query: &Option<String>, rules_store: &mut RuleStore) -> Node {
+    // Get scope node
+    // let mut scope_node = self.root_node();
+    if let Some(query_str) = scope_query {
+      // Apply the scope query in the source code and get the appropriate node
+      let tree_sitter_scope_query = rules_store.get_query(query_str);
+      if let Some((range, _)) =
+        &self
+          .root_node()
+          .get_match_for_query(&self.code(), tree_sitter_scope_query, true)
+      {
+        return get_node_for_range(self.root_node(), range.start_byte, range.end_byte);
+      }
+    }
+    self.root_node()
+  }
+
+  /// Apply all `rules` sequentially.
+  fn apply_rules(
+    &mut self, rules_store: &mut RuleStore, rules: &[Rule], parser: &mut Parser,
+    scope_query: Option<String>,
+  ) {
+    for rule in rules {
+      self.apply_rule(rule.to_owned(), rules_store, parser, &scope_query)
+    }
+  }
+}
+
+// Maintains the state of Piranha and the updated content of files in the source code.
+struct FlagCleaner {
+  // Maintains Piranha's state
+  rule_store: RuleStore,
+  // Path to source code folder
+  path_to_codebase: String,
+  // Files updated by Piranha.
+  relevant_files: HashMap<PathBuf, SourceCodeUnit>,
+}
+
+impl FlagCleaner {
+   fn get_updated_files(&self) -> Vec<SourceCodeUnit> {
+    self.relevant_files.values().cloned().collect_vec()
+  }
+
+  /// Performs cleanup related to stale flags
+   fn perform_cleanup(&mut self) {
+    // Setup the parser for the specific language
+    let mut parser = Parser::new();
+    parser
+      .set_language(self.rule_store.language())
+      .expect("Could not set the language for the parser.");
+
+    // Keep looping until new `global` rules are added.
+    loop {
+      let current_rules = self.rule_store.global_rules();
+
+      info!(
+        "{}",
+        format!("Number of global rules {}", current_rules.len())
+      );
+      // Iterate over each file containing the usage of the feature flag API
+      for (path, content) in self.get_files_containing_feature_flag_api_usage() {
+        self
+          .relevant_files
+          // Get the content of the file for `path` from the cache `relevant_files`
+          .entry(path.to_path_buf())
+          // Populate the cache (`relevant_files`) with the content, in case of cache miss (lazily)
+          .or_insert_with(|| {
+            // Create new source code unit
+            SourceCodeUnit::new(
+              &mut parser,
+              content,
+              &self.rule_store.input_substitutions(),
+              path.as_path(),
+            )
+          })
+          // Apply the rules to this file
+          .apply_rules(&mut self.rule_store, &current_rules, &mut parser, None);
+
+        // Break when a new `global` rule is added
+        if self.rule_store.global_rules().len() > current_rules.len() {
+          info!("Found a new global rule. Will start scanning all the files again.");
+          break;
+        }
+      }
+      // If no new `global_rules` were added, break.
+      if self.rule_store.global_rules().len() == current_rules.len() {
+        break;
+      }
+    }
+  }
+
+  /// Gets all the files from the code base that (i) have the language appropriate file extension, and (ii) contains the grep pattern.
+  /// Note that `WalkDir` traverses the directory with parallelism.
+   fn get_files_containing_feature_flag_api_usage(&self) -> HashMap<PathBuf, String> {
+    let pattern = self.get_grep_heuristics();
+    info!(
+      "{}",
+      format!("Searching for pattern {}", pattern.as_str()).green()
+    );
+    let files: HashMap<PathBuf, String> = WalkDir::new(&self.path_to_codebase)
+      // Walk over the entire code base
+      .into_iter()
+      // Ignore errors
+      .filter_map(|e| e.ok())
+      // Filter files with the desired extension
+      .filter(|de| {
+        de.path()
+          .extension()
+          .and_then(|e| {
+            e.to_str()
+              .filter(|x| x.eq(&self.rule_store.language_name()))
+          })
+          .is_some()
+      })
+      // Read the file
+      .map(|f| (f.path(), read_file(&f.path()).unwrap()))
+      // Filter the files containing the desired regex pattern
+      .filter(|x| pattern.is_match(x.1.as_str()))
+      .collect();
+    #[rustfmt::skip]
+    info!("{}", format!("Will parse and analyze {} files.", files.len()).green());
+    files
+  }
+
+  /// Instantiate Flag-cleaner
+   fn new(args: PiranhaArguments) -> Self {
+    let graph_rule_store = RuleStore::new(&args);
+    Self {
+      rule_store: graph_rule_store,
+      path_to_codebase: String::from(args.path_to_code_base()),
+      relevant_files: HashMap::new(),
+    }
+  }
+
+  /// To create the current set of global rules, certain substitutions were applied.
+  /// This method creates a regex pattern matching these substituted values.
+  ///
+  /// At the directory level, we would always look to perform global rules. However this is expensive because
+  /// it requires parsing each file. To overcome this, we apply this simple
+  /// heuristic to find the (upper bound) files that would match one of our current global rules.
+  /// This heuristic reduces the number of files to parse.
+  ///
+   fn get_grep_heuristics(&self) -> Regex {
+    let reg_x = self
+      .rule_store
+      .global_rules()
+      .iter()
+      .flat_map(|r| r.grep_heuristics())
+      .sorted()
+      //Remove duplicates
+      .dedup()
+      //FIXME: Dirty trick to remove true and false. Ideally, grep heuristic could be a field in itself for a rule.
+      // Since not all "holes" could be used as grep heuristic.
+      .filter(|x| !x.as_str().eq("true") && !x.as_str().eq("false"))
+      .join("|");
+    Regex::new(reg_x.as_str()).unwrap()
+  }
+}
diff --git a/generic/piranha/src/piranha/flag_cleaner.rs b/generic/piranha/src/piranha/flag_cleaner.rs
deleted file mode 100644
index 67bdcf357..000000000
--- a/generic/piranha/src/piranha/flag_cleaner.rs
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
-Copyright (c) 2022 Uber Technologies, Inc.
-
- <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
- except in compliance with the License. You may obtain a copy of the License at
- <p>http://www.apache.org/licenses/LICENSE-2.0
-
- <p>Unless required by applicable law or agreed to in writing, software distributed under the
- License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- express or implied. See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-use std::{collections::HashMap, path::PathBuf};
-
-use colored::Colorize;
-use itertools::Itertools;
-use jwalk::WalkDir;
-use log::info;
-use regex::Regex;
-use tree_sitter::Parser;
-
-use crate::utilities::read_file;
-
-use super::{
-  piranha_arguments::PiranhaArguments, rule_store::RuleStore, source_code_unit::SourceCodeUnit,
-};
-
-// Maintains the state of Piranha and the updated content of files in the source code.
-pub struct FlagCleaner {
-  // Maintains Piranha's state
-  rule_store: RuleStore,
-  // Path to source code folder
-  path_to_codebase: String,
-  // Files updated by Piranha.
-  relevant_files: HashMap<PathBuf, SourceCodeUnit>,
-}
-
-impl FlagCleaner {
-  pub fn get_updated_files(&self) -> Vec<SourceCodeUnit> {
-    self.relevant_files.values().cloned().collect_vec()
-  }
-
-  /// Performs cleanup related to stale flags
-  pub fn perform_cleanup(&mut self) {
-    // Setup the parser for the specific language
-    let mut parser = Parser::new();
-    parser
-      .set_language(self.rule_store.language())
-      .expect("Could not set the language for the parser.");
-
-    // Keep looping until new `global` rules are added.
-    loop {
-      let current_rules = self.rule_store.global_rules();
-
-      info!(
-        "{}",
-        format!("Number of global rules {}", current_rules.len())
-      );
-      // Iterate over each file containing the usage of the feature flag API
-      for (path, content) in self.get_files_containing_feature_flag_api_usage() {
-        self
-          .relevant_files
-          // Get the content of the file for `path` from the cache `relevant_files`
-          .entry(path.to_path_buf())
-          // Populate the cache (`relevant_files`) with the content, in case of cache miss (lazily)
-          .or_insert_with(|| {
-            // Create new source code unit
-            SourceCodeUnit::new(
-              &mut parser,
-              content,
-              &self.rule_store.input_substitutions(),
-              &path,
-            )
-          })
-          // Apply the rules to this file
-          .apply_rules(&mut self.rule_store, &current_rules, &mut parser, None);
-
-        // Break when a new `global` rule is added
-        if self.rule_store.global_rules().len() > current_rules.len() {
-          info!("Found a new global rule. Will start scanning all the files again.");
-          break;
-        }
-      }
-      // If no new `global_rules` were added, break.
-      if self.rule_store.global_rules().len() == current_rules.len() {
-        break;
-      }
-    }
-  }
-
-  /// Gets all the files from the code base that (i) have the language appropriate file extension, and (ii) contains the grep pattern.
-  /// Note that `WalkDir` traverses the directory with parallelism.
-  pub fn get_files_containing_feature_flag_api_usage(&self) -> HashMap<PathBuf, String> {
-    let pattern = self.get_grep_heuristics();
-    info!(
-      "{}",
-      format!("Searching for pattern {}", pattern.as_str()).green()
-    );
-    let files: HashMap<PathBuf, String> = WalkDir::new(&self.path_to_codebase)
-      // Walk over the entire code base
-      .into_iter()
-      // Ignore errors
-      .filter_map(|e| e.ok())
-      // Filter files with the desired extension
-      .filter(|de| {
-        de.path()
-          .extension()
-          .and_then(|e| {
-            e.to_str()
-              .filter(|x| x.eq(&self.rule_store.language_name()))
-          })
-          .is_some()
-      })
-      // Read the file
-      .map(|f| {
-        (
-          f.path(),
-          read_file(&f.path()).unwrap(),
-        )
-      })
-      // Filter the files containing the desired regex pattern
-      .filter(|x| pattern.is_match(x.1.as_str()))
-      .collect();
-    #[rustfmt::skip]
-    info!("{}", format!("Will parse and analyze {} files.", files.len()).green());
-    files
-  }
-
-  /// Instantiate Flag-cleaner
-  pub fn new(args: PiranhaArguments) -> Self {
-    let graph_rule_store = RuleStore::new(&args);
-    Self {
-      rule_store: graph_rule_store,
-      path_to_codebase: String::from(args.path_to_code_base()),
-      relevant_files: HashMap::new(),
-    }
-  }
-
-  /// To create the current set of global rules, certain substitutions were applied.
-  /// This method creates a regex pattern matching these substituted values.
-  ///
-  /// At the directory level, we would always look to perform global rules. However this is expensive because
-  /// it requires parsing each file. To overcome this, we apply this simple
-  /// heuristic to find the (upper bound) files that would match one of our current global rules.
-  /// This heuristic reduces the number of files to parse.
-  ///
-  pub fn get_grep_heuristics(&self) -> Regex {
-    let reg_x = self
-      .rule_store
-      .global_rules()
-      .iter()
-      .flat_map(|r| r.grep_heuristics())
-      .sorted()
-      //Remove duplicates
-      .dedup()
-      //FIXME: Dirty trick to remove true and false. Ideally, grep heuristic could be a field in itself for a rule.
-      // Since not all "holes" could be used as grep heuristic.
-      .filter(|x| !x.as_str().eq("true") && !x.as_str().eq("false"))
-      .join("|");
-    Regex::new(reg_x.as_str()).unwrap()
-  }
-}
diff --git a/generic/piranha/src/piranha/source_code_unit.rs b/generic/piranha/src/piranha/source_code_unit.rs
deleted file mode 100644
index 0fa30ef43..000000000
--- a/generic/piranha/src/piranha/source_code_unit.rs
+++ /dev/null
@@ -1,421 +0,0 @@
-/*
-Copyright (c) 2022 Uber Technologies, Inc.
-
- <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
- except in compliance with the License. You may obtain a copy of the License at
- <p>http://www.apache.org/licenses/LICENSE-2.0
-
- <p>Unless required by applicable law or agreed to in writing, software distributed under the
- License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- express or implied. See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-use crate::models::rule::{Rule, RuleHelper};
-use crate::piranha::rule_store::{RuleStore, GLOBAL, PARENT};
-use crate::utilities::tree_sitter_utilities::{PiranhaRuleMatcher, TreeSitterHelpers};
-use colored::Colorize;
-use log::info;
-use std::collections::VecDeque;
-use std::{collections::HashMap, fs, path::PathBuf};
-use tree_sitter::{InputEdit, Node, Parser, Point, Range, Tree};
-
-// Maintains the updated source code content and AST of the file
-#[derive(Clone)]
-pub struct SourceCodeUnit {
-  // The tree representing the file
-  ast: Tree,
-  // The content of a file
-  code: String,
-  // The tag substitution cache.
-  // This map is looked up to instantiate new rules.
-  substitutions: HashMap<String, String>,
-  // The path to the source code.
-  path: PathBuf,
-}
-
-impl SourceCodeUnit {
-  /// Writes the current contents of `code` to the file system.
-  pub fn persist(&self) {
-    fs::write(&self.path, self.code.as_str()).expect("Unable to Write file");
-  }
-
-  /// Applies an edit to the source code unit
-  /// # Arguments
-  /// * `replace_range` - the range of code to be replaced
-  /// * `replacement_str` - the replacement string
-  /// * `parser`
-  ///
-  /// # Returns
-  /// The `edit:InputEdit` performed.
-  ///
-  /// Note - Causes side effect. - Updates `self.ast` and `self.code`
-  fn apply_edit(
-    &mut self, replace_range: Range, replacement_str: String, parser: &mut Parser,
-  ) -> InputEdit {
-    // Get the tree_sitter's input edit representation
-    let (new_source_code, edit) = self.get_edit(replace_range, &replacement_str);
-    // Apply edit to the tree
-    self.ast.edit(&edit);
-    // Create a new updated tree from the previous tree
-    let new_tree = parser
-      .parse(&new_source_code, Some(&self.ast))
-      .expect("Could not generate new tree!");
-    self.ast = new_tree;
-    self.code = new_source_code;
-    edit
-  }
-
-  /// Will apply the `rule` to all of its occurrences in the source code unit.
-  fn apply_rule(
-    &mut self, rule: Rule, rules_store: &mut RuleStore, parser: &mut Parser,
-    scope_query: &Option<String>,
-  ) {
-    loop {
-      if !self.apply_rule_to_first_match(rule.clone(), rules_store, parser, scope_query) {
-        break;
-      }
-    }
-  }
-
-  // Applies the rule to the first match in the source code
-  fn apply_rule_to_first_match(
-    &mut self, rule: Rule, rules_store: &mut RuleStore, parser: &mut Parser,
-    scope_query: &Option<String>,
-  ) -> bool {
-    let scope_node = self.get_scope_node(scope_query, rules_store);
-
-    let mut any_match = false;
-
-    // Match the rule "anywhere" inside the scope_node
-    if let Some((range, rpl, code_snippets_by_tag)) =
-      self.get_any_match_for_rule(&rule, rules_store, scope_node, true)
-    {
-      any_match = true;
-      // Get the edit for applying the matched rule to the source code
-      let edit = self.apply_edit(range, rpl, parser);
-
-      // Add all the (code_snippet, tag) mapping to the substitution table.
-      self.substitutions.extend(code_snippets_by_tag);
-
-      let mut current_edit = edit;
-      let mut current_rule = rule.clone();
-      let mut next_rules_stack: VecDeque<(String, Rule)> = VecDeque::new();
-
-      // Perform the parent edits, while queueing the Method and Class level edits.
-      // let file_level_scope_names = [METHOD, CLASS];
-      loop {
-        // Get all the (next) rules that could be after applying the current rule (`rule`).
-        let next_rules_by_scope = rules_store.get_next(&current_rule, &self.substitutions);
-
-        // Adds "Method" and "Class" rules to the stack
-        self.add_rules_to_stack(
-          &next_rules_by_scope,
-          current_edit,
-          rules_store,
-          &mut next_rules_stack,
-        );
-
-        // Add Global rules as seed rules
-        for r in &next_rules_by_scope[GLOBAL] {
-          rules_store.add_to_global_rules(r, &self.substitutions);
-        }
-
-        // Process the parent
-        // Find the rules to be applied in the "Parent" scope that match any parent (context) of the changed node in the previous edit
-        if let Some((c_range, replacement_str, matched_rule, code_snippets_by_tag_parent_rule)) =
-          self.match_rules_to_context(current_edit, rules_store, &next_rules_by_scope[PARENT])
-        {
-          #[rustfmt::skip]
-          info!( "{}", format!( "Cleaning up the context, by applying the rule - {}", matched_rule.name()).green());
-          // Apply the matched rule to the parent
-          current_edit = self.apply_edit(c_range, replacement_str, parser);
-          current_rule = matched_rule;
-          // Add the (tag, code_snippet) mapping to substitution table.
-          self.substitutions.extend(code_snippets_by_tag_parent_rule);
-        } else {
-          // No more parents found for cleanup
-          break;
-        }
-      }
-
-      // Apply the next rules from the stack
-      for (sq, rle) in next_rules_stack {
-        self.apply_rule(rle, rules_store, parser, &Some(sq.to_string()));
-      }
-    }
-    any_match
-  }
-
-  /// Adds the "Method" and "Class" scoped next rules to the queue.
-  fn add_rules_to_stack(
-    &mut self, next_rules_by_scope: &HashMap<String, Vec<Rule>>, current_edit: InputEdit,
-    rules_store: &mut RuleStore, stack: &mut VecDeque<(String, Rule)>,
-  ) {
-    for (scope_level, rules) in next_rules_by_scope {
-      // Scope level is not "PArent" or "Global"
-      if ![PARENT, GLOBAL].contains(&scope_level.as_str()) {
-        for rule in rules {
-          // Generate the scope query for the previously applied edit
-          // This query will precisely capture the enclosing method / class.
-          let scope_query = self.get_scope_query(scope_level, current_edit, rules_store);
-          // Add Method and Class scoped rules to the queue
-          stack.push_front((
-            scope_query.to_string(),
-            rule.instantiate(&self.substitutions),
-          ));
-        }
-      }
-    }
-  }
-
-  fn get_scope_node(&self, scope_query: &Option<String>, rules_store: &mut RuleStore) -> Node {
-    // Get scope node
-    let mut scope_node = self.ast.root_node();
-    if let Some(query_str) = scope_query {
-      // Apply the scope query in the source code and get the appropriate node
-      let tree_sitter_scope_query = rules_store.get_query(query_str);
-      if let Some((range, _)) =
-        &self
-          .ast
-          .root_node()
-          .get_match_for_query(&self.code, tree_sitter_scope_query, true)
-      {
-        scope_node = self.get_node_for_range(range.start_byte, range.end_byte);
-      }
-    }
-    scope_node
-  }
-
-  /// Apply all `rules` sequentially.
-  pub(crate) fn apply_rules(
-    &mut self, rules_store: &mut RuleStore, rules: &Vec<Rule>, parser: &mut Parser,
-    scope_query: Option<String>,
-  ) {
-    for rule in rules.clone() {
-      self.apply_rule(rule.clone(), rules_store, parser, &scope_query)
-    }
-  }
-
-  /// Get the smallest node within `self` that spans the given range.
-  fn get_node_for_range(&self, start_byte: usize, end_byte: usize) -> Node {
-    self
-      .ast
-      .root_node()
-      .descendant_for_byte_range(start_byte, end_byte)
-      .unwrap()
-  }
-
-  /// Generate a tree-sitter based query representing the scope of the previous edit.
-  /// We generate these scope queries by matching the rules provided in `<lang>_scopes.toml`.
-  fn get_scope_query(
-    &self, scope_level: &str, previous_edit: InputEdit, rules_store: &mut RuleStore,
-  ) -> String {
-    let mut changed_node =
-      self.get_node_for_range(previous_edit.start_byte, previous_edit.new_end_byte);
-
-    // Get the scope matchers for `scope_level` from the `scope_config.toml`.
-    let scope_matchers = rules_store.get_scope_query_generators(scope_level);
-
-    // Match the `scope_matcher.matcher` to the parent
-    while let Some(parent) = changed_node.parent() {
-      for m in &scope_matchers {
-        if let Some((_, captures_by_tag)) =
-          parent.get_match_for_query(&self.code, rules_store.get_query(&m.matcher()), false)
-        {
-          // Generate the scope query for the specific context by substituting the
-          // the tags with code snippets appropriately in the `generator` query.
-          return m.generator().substitute_tags(&captures_by_tag);
-        } else {
-          changed_node = parent;
-        }
-      }
-    }
-    panic!("Could not create scope query for {:?}", scope_level);
-  }
-
-  // Apply all the `rules` to the node, parent, grand parent and great grand parent.
-  // Short-circuit on the first match.
-  fn match_rules_to_context(
-    &mut self, previous_edit: InputEdit, rules_store: &mut RuleStore, rules: &Vec<Rule>,
-  ) -> Option<(Range, String, Rule, HashMap<String, String>)> {
-    // Context contains -  the changed node in the previous edit, its's parent, grand parent and great grand parent
-    let context = || self.get_context(previous_edit);
-    for rule in rules {
-      for ancestor in &context() {
-        if let Some((range, replacement, captures_by_tag)) =
-          self.get_any_match_for_rule(rule, rules_store, *ancestor, false)
-        {
-          return Some((range, replacement, rule.clone(), captures_by_tag));
-        }
-      }
-    }
-    None
-  }
-
-  /// Returns the node, its parent, grand parent and great grand parent
-  fn get_context(&self, previous_edit: InputEdit) -> Vec<Node> {
-    let changed_node =
-      self.get_node_for_range(previous_edit.start_byte, previous_edit.new_end_byte);
-    // Add parent of the changed node to the context
-    let mut context = vec![changed_node];
-    if let Some(parent) = changed_node.parent() {
-      context.push(parent);
-      // Add grand parent of the changed node to the context
-      if let Some(grand_parent) = parent.parent() {
-        context.push(grand_parent);
-        // Add great grand parent of the changed node to the context
-        if let Some(great_grand_parent) = grand_parent.parent() {
-          context.push(great_grand_parent);
-        }
-      }
-    }
-    context
-  }
-
-  pub(crate) fn new(
-    parser: &mut Parser, code: String, substitutions: &HashMap<String, String>, path: &PathBuf,
-  ) -> Self {
-    let ast = parser.parse(&code, None).expect("Could not parse code");
-    Self {
-      ast,
-      code,
-      substitutions: substitutions.clone(),
-      path: path.to_path_buf(),
-    }
-  }
-
-  /// Gets the first match for the rule in `self`
-  fn get_any_match_for_rule(
-    &self, rule: &Rule, rule_store: &mut RuleStore, node: Node, recursive: bool,
-  ) -> Option<(Range, String, HashMap<String, String>)> {
-    // Get all matches for the query in the given scope `node`.
-    let all_query_matches = node.get_all_matches_for_query(
-      self.code.clone(),
-      rule_store.get_query(&rule.get_query()),
-      recursive,
-      Some(rule.replace_node()),
-    );
-
-    // Return the first match that satisfies constraint of the rule
-    for (range, tag_substitutions) in all_query_matches {
-      let matched_node = self.get_node_for_range(range.start_byte, range.end_byte);
-      if self.satisfies_constraint(matched_node, rule, &tag_substitutions, rule_store) {
-        let replacement = rule.replace().substitute_tags(&tag_substitutions);
-        return Some((range, replacement, tag_substitutions));
-      }
-    }
-    None
-  }
-
-  /// Checks if the node satisfies the constraints.
-  /// Constraint has two parts (i) `constraint.matcher` (ii) `constraint.query`.
-  /// This function traverses the ancestors of the given `node` until `constraint.matcher` matches i.e. finds scope for constraint.
-  /// Within this scope it checks if the `constraint.query` DOES NOT MATCH any sub-tree.
-  fn satisfies_constraint(
-    &self, node: Node, rule: &Rule, capture_by_tags: &HashMap<String, String>,
-    rule_store: &mut RuleStore,
-  ) -> bool {
-    let mut current_node = node;
-    // Get the scope of the predicate
-    for constraint in rule.constraints() {
-      // Loop till you find a parent for the current node
-      while let Some(parent) = current_node.parent() {
-        // Check if the parent matches the `rule.constraint.matcher`
-        // This is the scope in which the constraint query will be applied.
-        if let Some((range, _)) = parent.get_match_for_query(
-          &self.code,
-          &constraint.matcher().create_query(rule_store.language()),
-          false,
-        ) {
-          let scope_node = self.get_node_for_range(range.start_byte, range.end_byte);
-          // Apply each query within the `scope_node`
-          for query_with_holes in constraint.queries() {
-            let query_str = query_with_holes.substitute_tags(capture_by_tags);
-            let query = &rule_store.get_query(&query_str);
-            // If this query matches anywhere within the scope, return false.
-            if scope_node
-              .get_match_for_query(&self.code, query, true)
-              .is_some()
-            {
-              return false;
-            }
-          }
-          break;
-        }
-        current_node = parent;
-      }
-    }
-    true
-  }
-
-  /// Replaces the given byte range (`replace_range`) with the `replacement`.
-  /// Returns tree-sitter's edit representation along with updated source code.
-  /// Note: This method does not update `self`.
-  pub fn get_edit(&self, replace_range: Range, replacement: &str) -> (String, InputEdit) {
-    let original_source_code = self.code.clone();
-
-    // Create the new source code content by appropriately
-    // replacing the range with the replacement string.
-    let updated_source_code = [
-      &original_source_code[..replace_range.start_byte],
-      replacement,
-      &original_source_code[replace_range.end_byte..],
-    ]
-    .concat();
-
-    // Log the edit
-    let replaced_code_snippet =
-      &original_source_code[replace_range.start_byte..replace_range.end_byte];
-
-    #[rustfmt::skip]
-    info!("{} at ({:?}) -\n {}", if replacement.is_empty() { "Delete code" } else {"Update code" }.green(), ((&replace_range.start_point.row, &replace_range.start_point.column), (&replace_range.end_point.row, &replace_range.end_point.column)), if !replacement.is_empty() {format!("{}\n to \n{}",replaced_code_snippet.italic(),replacement.italic())} else {format!("{} ", replaced_code_snippet.italic())});
-
-    let original_bytes = &original_source_code.as_bytes().to_vec();
-
-    let edit =
-      Self::get_tree_sitter_edit(replace_range, replacement.as_bytes().len(), original_bytes);
-
-    (updated_source_code, edit)
-  }
-
-  // Finds the position (col and row number) for a given offset.
-  fn position_for_offset(input: &Vec<u8>, offset: usize) -> Point {
-    let mut result = Point { row: 0, column: 0 };
-    for c in &input[0..offset] {
-      if *c as char == '\n' {
-        result.row += 1;
-        result.column = 0;
-      } else {
-        result.column += 1;
-      }
-    }
-    result
-  }
-
-  // Creates the InputEdit as per the tree-sitter api documentation.
-  fn get_tree_sitter_edit(
-    replace_range: Range, len_of_replacement: usize, source_code_bytes: &Vec<u8>,
-  ) -> InputEdit {
-    let start_byte = replace_range.start_byte;
-    let old_end_byte = replace_range.end_byte;
-    let new_end_byte = start_byte + len_of_replacement;
-    InputEdit {
-      start_byte,
-      old_end_byte,
-      new_end_byte,
-      start_position: Self::position_for_offset(source_code_bytes, start_byte),
-      old_end_position: Self::position_for_offset(source_code_bytes, old_end_byte),
-      new_end_position: Self::position_for_offset(source_code_bytes, new_end_byte),
-    }
-  }
-
-  #[cfg(test)] // Rust analyzer FP
-  pub fn code(&self) -> String {
-    String::from(&self.code)
-  }
-  #[cfg(test)] // Rust analyzer FP
-  pub fn path(&self) -> &PathBuf {
-    &self.path
-  }
-}
diff --git a/generic/piranha/src/utilities/mod.rs b/generic/piranha/src/utilities/mod.rs
index b5b75c925..68a9b548e 100644
--- a/generic/piranha/src/utilities/mod.rs
+++ b/generic/piranha/src/utilities/mod.rs
@@ -11,7 +11,7 @@ Copyright (c) 2022 Uber Technologies, Inc.
  limitations under the License.
 */
 
-pub mod tree_sitter_utilities;
+pub(crate) mod tree_sitter_utilities;
 
 use std::collections::HashMap;
 #[cfg(test)]
@@ -22,7 +22,7 @@ use std::io::{BufReader, Read};
 use std::path::PathBuf;
 
 // Reads a file.
-pub fn read_file(file_path: &PathBuf) -> Result<String, String> {
+pub(crate) fn read_file(file_path: &PathBuf) -> Result<String, String> {
   File::open(&file_path)
     .map(|file| {
       let mut content = String::new();
@@ -33,7 +33,7 @@ pub fn read_file(file_path: &PathBuf) -> Result<String, String> {
 }
 
 // Reads a toml file. In case of error, it returns a default value (if return_default is true) else panics.
-pub fn read_toml<T>(file_path: &PathBuf, return_default: bool) -> T
+pub(crate) fn read_toml<T>(file_path: &PathBuf, return_default: bool) -> T
 where
   T: serde::de::DeserializeOwned + Default,
 {
@@ -52,7 +52,7 @@ where
   }
 }
 
-pub trait MapOfVec<T, V> {
+pub(crate) trait MapOfVec<T, V> {
   fn collect(&mut self, key: T, value: V);
 }
 
@@ -66,7 +66,7 @@ impl<T: Hash + Eq, U> MapOfVec<T, U> for HashMap<T, Vec<U>> {
 }
 
 /// Initialize logger.
-pub fn initialize_logger(is_test: bool) {
+pub(crate) fn initialize_logger(is_test: bool) {
   let log_file = OpenOptions::new()
     .write(true)
     .create(true) // Create a log file if it doesn't exists
@@ -80,17 +80,18 @@ pub fn initialize_logger(is_test: bool) {
     .try_init();
 }
 
-/// Compares two strings, ignoring new lines, and space.
+/// Compares two strings, ignoring whitespace
 #[cfg(test)] // Rust analyzer FP
-pub fn eq_without_whitespace(s1: &str, s2: &str) -> bool {
-  s1.replace('\n', "")
-    .replace(' ', "")
-    .eq(&s2.replace('\n', "").replace(' ', ""))
+pub(crate) fn eq_without_whitespace(s1: &str, s2: &str) -> bool {
+  s1.split_whitespace()
+    .collect::<String>()
+    .eq(&s2.split_whitespace().collect::<String>())
 }
 
 /// Checks if the given `dir_entry` is a file named `file_name`
 #[cfg(test)] // Rust analyzer FP
-pub fn has_name(dir_entry: &DirEntry, file_name: &str) -> bool {
+pub(crate) fn has_name(dir_entry: &DirEntry, file_name: &str) -> bool {
+  println!("{:?}", dir_entry);
   dir_entry
     .path()
     .file_name()
@@ -100,7 +101,7 @@ pub fn has_name(dir_entry: &DirEntry, file_name: &str) -> bool {
 
 /// Returns the file with the given name within the given directory.
 #[cfg(test)] // Rust analyzer FP
-pub fn find_file(input_dir: &PathBuf, name: &str) -> PathBuf {
+pub(crate) fn find_file(input_dir: &PathBuf, name: &str) -> PathBuf {
   fs::read_dir(input_dir)
     .unwrap()
     .filter_map(|d| d.ok())
@@ -108,3 +109,7 @@ pub fn find_file(input_dir: &PathBuf, name: &str) -> PathBuf {
     .unwrap()
     .path()
 }
+
+#[cfg(test)]
+#[path = "unit_tests/utilities_test.rs"]
+mod utilities_test;
diff --git a/generic/piranha/src/utilities/tree_sitter_utilities.rs b/generic/piranha/src/utilities/tree_sitter_utilities.rs
index 16f84122f..9f330cc67 100644
--- a/generic/piranha/src/utilities/tree_sitter_utilities.rs
+++ b/generic/piranha/src/utilities/tree_sitter_utilities.rs
@@ -13,16 +13,23 @@ Copyright (c) 2022 Uber Technologies, Inc.
 
 //! Defines the traits containing with utility functions that interface with tree-sitter.
 
-use crate::utilities::MapOfVec;
+use crate::{
+  models::{rule::Rule, rule_store::RuleStore, source_code_unit::SourceCodeUnit},
+  utilities::MapOfVec,
+};
+use colored::Colorize;
 use itertools::Itertools;
+use log::info;
 use std::collections::HashMap;
-use tree_sitter::{Language, Node, Query, QueryCursor, Range};
+#[cfg(test)]
+use tree_sitter::Parser;
+use tree_sitter::{InputEdit, Language, Node, Point, Query, QueryCursor, Range};
 
 extern "C" {
   fn tree_sitter_java() -> Language;
 }
 
-pub trait TreeSitterHelpers {
+pub(crate) trait TreeSitterHelpers {
   /// Gets the tree-sitter language model.
   fn get_language(&self) -> Language;
   /// Compiles query string to `tree_sitter::Query`
@@ -48,8 +55,10 @@ impl TreeSitterHelpers for String {
 }
 
 #[rustfmt::skip]
-pub trait PiranhaRuleMatcher {
+pub(crate) trait PiranhaHelpers {
 
+    /// Checks if the given rule satisfies the constraint of the rule, under the substitutions obtained upon matching `rule.query`
+    fn satisfies_constraint(&self, source_code_unit: SourceCodeUnit, rule: &Rule, substitutions: &HashMap<String, String>,rule_store: &mut RuleStore,) -> bool ;
     /// Applies the query upon `self`, and gets the first match
     /// # Arguments
     /// * `source_code` - the corresponding source code string for the node.
@@ -73,13 +82,24 @@ pub trait PiranhaRuleMatcher {
     fn get_match_for_query(&self, source_code: &str, query: &Query, recursive: bool) -> Option<(Range, HashMap<String, String>)>;
 }
 
-impl PiranhaRuleMatcher for Node<'_> {
+impl PiranhaHelpers for Node<'_> {
+  /// Checks if the given rule satisfies the constraint of the rule, under the substitutions obtained upon matching `rule.query`
+  fn satisfies_constraint(
+    &self, source_code_unit: SourceCodeUnit, rule: &Rule, substitutions: &HashMap<String, String>,
+    rule_store: &mut RuleStore,
+  ) -> bool {
+    rule.constraints().iter().all(|constraint| {
+      constraint.is_satisfied(*self, source_code_unit.clone(), rule_store, substitutions)
+    })
+  }
+
   fn get_match_for_query(
     &self, source_code: &str, query: &Query, recursive: bool,
   ) -> Option<(Range, HashMap<String, String>)> {
     self
       .get_all_matches_for_query(source_code.to_string(), query, recursive, None)
-      .first().cloned()
+      .first()
+      .cloned()
   }
 
   fn get_all_matches_for_query(
@@ -172,3 +192,112 @@ impl PiranhaRuleMatcher for Node<'_> {
     output
   }
 }
+
+/// Replaces the given byte range (`replace_range`) with the `replacement`.
+/// Returns tree-sitter's edit representation along with updated source code.
+/// Note: This method does not update `self`.
+pub(crate) fn get_tree_sitter_edit(
+  code: String, replace_range: Range, replacement: &str,
+) -> (String, InputEdit) {
+  // Log the edit
+  let replaced_code_snippet = &code[replace_range.start_byte..replace_range.end_byte];
+
+  #[rustfmt::skip]
+    info!("{} at ({:?}) -\n {}", if replacement.is_empty() { "Delete code" } else {"Update code" }.green(), ((&replace_range.start_point.row, &replace_range.start_point.column), (&replace_range.end_point.row, &replace_range.end_point.column)), if !replacement.is_empty() {format!("{}\n to \n{}",replaced_code_snippet.italic(),replacement.italic())} else {format!("{} ", replaced_code_snippet.italic())});
+
+  (
+    // Create the new source code content by appropriately
+    // replacing the range with the replacement string.
+    [
+      &code[..replace_range.start_byte],
+      replacement,
+      &code[replace_range.end_byte..],
+    ]
+    .concat(),
+    // Tree-sitter edit
+    _get_tree_sitter_edit(replace_range, replacement.as_bytes().len(), code.as_bytes()),
+  )
+}
+
+// Finds the position (col and row number) for a given offset.
+fn position_for_offset(input: &[u8], offset: usize) -> Point {
+  let mut result = Point { row: 0, column: 0 };
+  for c in &input[0..offset] {
+    if *c as char == '\n' {
+      result.row += 1;
+      result.column = 0;
+    } else {
+      result.column += 1;
+    }
+  }
+  result
+}
+
+// Creates the InputEdit as per the tree-sitter api documentation.
+fn _get_tree_sitter_edit(
+  replace_range: Range, len_of_replacement: usize, source_code_bytes: &[u8],
+) -> InputEdit {
+  let start_byte = replace_range.start_byte;
+  let old_end_byte = replace_range.end_byte;
+  let new_end_byte = start_byte + len_of_replacement;
+  InputEdit {
+    start_byte,
+    old_end_byte,
+    new_end_byte,
+    start_position: position_for_offset(source_code_bytes, start_byte),
+    old_end_position: position_for_offset(source_code_bytes, old_end_byte),
+    new_end_position: position_for_offset(source_code_bytes, new_end_byte),
+  }
+}
+
+pub(crate) fn substitute_tags(s: String, substitutions: &HashMap<String, String>) -> String {
+  let mut output = s;
+  for (tag, substitute) in substitutions {
+    // Before replacing the key, it is transformed to a tree-sitter tag by adding `@` as prefix
+    let key = format!("@{}", tag);
+    output = output.replace(&key, substitute)
+  }
+  output
+}
+
+/// Get the smallest node within `self` that spans the given range.
+pub(crate) fn get_node_for_range(root_node: Node, start_byte: usize, end_byte: usize) -> Node {
+  root_node
+    .descendant_for_byte_range(start_byte, end_byte)
+    .unwrap()
+}
+
+/// Returns the node, its parent, grand parent and great grand parent
+pub(crate) fn get_context(
+  root_node: Node, previous_edit_start: usize, previous_edit_end: usize,
+) -> Vec<Node> {
+  // let root = self.root_node();
+  let changed_node = get_node_for_range(root_node, previous_edit_start, previous_edit_end);
+  // Add parent of the changed node to the context
+  let mut context = vec![changed_node];
+  if let Some(parent) = changed_node.parent() {
+    context.push(parent);
+    // Add grand parent of the changed node to the context
+    if let Some(grand_parent) = parent.parent() {
+      context.push(grand_parent);
+      // Add great grand parent of the changed node to the context
+      if let Some(great_grand_parent) = grand_parent.parent() {
+        context.push(great_grand_parent);
+      }
+    }
+  }
+  context
+}
+
+#[cfg(test)]
+pub(crate) fn get_parser(language: String) -> Parser {
+  let mut parser = Parser::new();
+  parser
+    .set_language(language.get_language())
+    .expect("Could not set the language for the parser.");
+  parser
+}
+
+#[cfg(test)]
+#[path = "unit_tests/tree_sitter_utilities_test.rs"]
+mod tree_sitter_utilities_test;


Test patch:
diff --git a/generic/piranha/src/models/unit_tests/rule_test.rs b/generic/piranha/src/models/unit_tests/rule_test.rs
new file mode 100644
index 000000000..9b90fb204
--- /dev/null
+++ b/generic/piranha/src/models/unit_tests/rule_test.rs
@@ -0,0 +1,212 @@
+use {
+  super::Rule,
+  crate::{
+    models::{constraint::Constraint, rule_store::RuleStore, source_code_unit::SourceCodeUnit},
+    utilities::tree_sitter_utilities::get_parser,
+  },
+  std::collections::HashMap,
+  std::path::PathBuf,
+};
+
+/// Tests whether a valid rule can be correctly instantiated given valid substitutions.
+#[test]
+fn test_rule_try_instantiate_positive() {
+  let rule = Rule::new("test","(((assignment_expression left: (_) @a.lhs right: (_) @a.rhs) @abc) (#eq? @a.lhs \"@variable_name\"))",
+        "@abc", "",Some(vec![String::from("variable_name")]), None);
+  let substitutions: HashMap<String, String> = HashMap::from([
+    (String::from("variable_name"), String::from("foobar")),
+    (String::from("@a.lhs"), String::from("something")), // Should not substitute, since it `a.lhs` is not in `rule.holes`
+  ]);
+  let instantiated_rule = rule.try_instantiate(&substitutions);
+  assert!(instantiated_rule.is_ok());
+  assert_eq!(
+    instantiated_rule.ok().unwrap().query(),
+    "(((assignment_expression left: (_) @a.lhs right: (_) @a.rhs) @abc) (#eq? @a.lhs \"foobar\"))"
+  )
+}
+
+/// Tests whether a valid rule can be is *not* instantiated given invalid substitutions.
+#[test]
+fn test_rule_try_instantiate_negative() {
+  let rule = Rule::new("test","(((assignment_expression left: (_) @a.lhs right: (_) @a.rhs) @abc) (#eq? @a.lhs \"@variable_name\"))",
+        "abc", "",Some(vec![String::from("variable_name")]), None);
+  let substitutions: HashMap<String, String> = HashMap::from([
+    (String::from("@a.lhs"), String::from("something")), // Should not substitute, since it `a.lhs` is not in `rule.holes`
+  ]);
+  let instantiated_rule = rule.try_instantiate(&substitutions);
+  assert!(instantiated_rule.is_err());
+}
+
+/// Positive tests for `rule.get_edit` method for given rule and input source code.
+#[test]
+fn test_get_edit_positive_recursive() {
+  let rule = Rule::new("test", "(
+                           ((local_variable_declaration
+                                           declarator: (variable_declarator
+                                                               name: (_) @variable_name
+                                                               value: [(true) (false)] @init)) @variable_declaration)
+                           )", "variable_declaration", "" ,None, 
+                           Some(vec![
+                          Constraint::new(String::from("(method_declaration) @md"),
+                            vec![String::from("(
+                              ((assignment_expression
+                                              left: (_) @a.lhs
+                                              right: (_) @a.rhs) @assignment)
+                              (#eq? @a.lhs \"@variable_name\")
+                              (#not-eq? @a.rhs \"@init\")
+                            )")]),
+                          ]));
+  let source_code = "class Test {
+          pub void foobar(){
+            boolean isFlagTreated = true;
+            isFlagTreated = true;
+            if (isFlagTreated) {
+              // Do something;
+            }
+          }
+        }";
+
+  let mut rule_store = RuleStore::dummy();
+
+  let mut parser = get_parser(String::from("java"));
+
+  let source_code_unit = SourceCodeUnit::new(
+    &mut parser,
+    source_code.to_string(),
+    &HashMap::new(),
+    PathBuf::new().as_path(),
+  );
+  let node = source_code_unit.root_node();
+  let edit = rule.get_edit(&source_code_unit, &mut rule_store, node, true);
+  assert!(edit.is_some());
+}
+
+/// Negative tests for `rule.get_edit` method for given rule and input source code.
+#[test]
+fn test_get_edit_negative_recursive() {
+  let rule = Rule::new("test", "(
+                           ((local_variable_declaration
+                                           declarator: (variable_declarator
+                                                               name: (_) @variable_name
+                                                               value: [(true) (false)] @init)) @variable_declaration)
+                           )", "variable_declaration", "" ,None, 
+                           Some(vec![
+                          Constraint::new(String::from("(method_declaration) @md"),
+                            vec![String::from("(
+                              ((assignment_expression
+                                              left: (_) @a.lhs
+                                              right: (_) @a.rhs) @assignment)
+                              (#eq? @a.lhs \"@variable_name\")
+                              (#not-eq? @a.rhs \"@init\")
+                            )")]),
+                          ]));
+  let source_code = "class Test {
+          pub void foobar(){
+            boolean isFlagTreated = true;
+            isFlagTreated = false;
+            if (isFlagTreated) {
+              // Do something;
+            }
+          }
+        }";
+
+  let mut rule_store = RuleStore::dummy();
+  let mut parser = get_parser(String::from("java"));
+
+  let source_code_unit = SourceCodeUnit::new(
+    &mut parser,
+    source_code.to_string(),
+    &HashMap::new(),
+    PathBuf::new().as_path(),
+  );
+  let node = source_code_unit.root_node();
+  let edit = rule.get_edit(&source_code_unit, &mut rule_store, node, true);
+  assert!(edit.is_none());
+}
+
+/// Positive tests for `rule.get_edit_for_context` method for given rule and input source code.
+#[test]
+fn test_get_edit_for_context_positive() {
+  let rule = Rule::new(
+    "test",
+    "(
+          (binary_expression
+              left : (_)* @lhs
+              operator:\"&&\"
+              right: [(true) (parenthesized_expression (true))]
+          )
+      @binary_expression)",
+    "binary_expression",
+    "",
+    None,
+    None,
+  );
+
+  let source_code = "class A {
+          boolean f = something && true;
+        }";
+
+  let mut rule_store = RuleStore::dummy();
+
+  let mut parser = get_parser(String::from("java"));
+
+  let source_code_unit = SourceCodeUnit::new(
+    &mut parser,
+    source_code.to_string(),
+    &HashMap::new(),
+    PathBuf::new().as_path(),
+  );
+  let edit = Rule::get_edit_for_context(
+    &source_code_unit,
+    41 as usize,
+    44 as usize,
+    &mut rule_store,
+    &vec![rule],
+  );
+  // let edit = rule.get_edit(&source_code_unit, &mut rule_store, node, true);
+  assert!(edit.is_some());
+}
+
+/// Negative tests for `rule.get_edit_for_context` method for given rule and input source code.
+#[test]
+fn test_get_edit_for_context_negative() {
+  let rule = Rule::new(
+    "test",
+    "(
+          (binary_expression
+              left : (_)* @lhs
+              operator:\"&&\"
+              right: [(true) (parenthesized_expression (true))]
+          )
+      @binary_expression)",
+    "binary_expression",
+    "",
+    None,
+    None,
+  );
+
+  let source_code = "class A {
+          boolean f = true;
+          boolean x = something && true;
+        }";
+
+  let mut rule_store = RuleStore::dummy();
+
+  let mut parser = get_parser(String::from("java"));
+
+  let source_code_unit = SourceCodeUnit::new(
+    &mut parser,
+    source_code.to_string(),
+    &HashMap::new(),
+    PathBuf::new().as_path(),
+  );
+  let edit = Rule::get_edit_for_context(
+    &source_code_unit,
+    29 as usize,
+    33 as usize,
+    &mut rule_store,
+    &vec![rule],
+  );
+  // let edit = rule.get_edit(&source_code_unit, &mut rule_store, node, true);
+  assert!(edit.is_none());
+}
diff --git a/generic/piranha/src/models/unit_tests/scopes_test.rs b/generic/piranha/src/models/unit_tests/scopes_test.rs
new file mode 100644
index 000000000..b3cb06b3f
--- /dev/null
+++ b/generic/piranha/src/models/unit_tests/scopes_test.rs
@@ -0,0 +1,169 @@
+use {
+  super::{ScopeGenerator, ScopeQueryGenerator},
+  crate::{
+    models::{rule_store::RuleStore, source_code_unit::SourceCodeUnit},
+    utilities::eq_without_whitespace,
+    utilities::tree_sitter_utilities::get_parser,
+  },
+  std::{collections::HashMap, path::PathBuf},
+};
+
+/// Positive test for the generated scope query, given scope generators, source code and position of pervious edit.
+#[test]
+fn test_get_scope_query_positive() {
+  let scope_generator_method = ScopeGenerator::new(
+    "Method",
+    vec![ScopeQueryGenerator::new(
+      "((method_declaration 
+          name : (_) @n
+                parameters : (formal_parameters
+                    (formal_parameter type:(_) @t0)
+                        (formal_parameter type:(_) @t1)
+                        (formal_parameter type:(_) @t2))) @xd2)",
+      "(((method_declaration 
+                        name : (_) @z
+                              parameters : (formal_parameters
+                                  (formal_parameter type:(_) @r0)
+                                      (formal_parameter type:(_) @r1)
+                                      (formal_parameter type:(_) @r2))) @qd)
+                  (#eq? @z \"@n\")
+                  (#eq? @r0 \"@t0\")
+                  (#eq? @r1 \"@t1\")
+                  (#eq? @r2 \"@t2\")
+                  )",
+    )],
+  );
+
+  let scope_generator_class = ScopeGenerator::new(
+    "Class",
+    vec![ScopeQueryGenerator::new(
+      "(class_declaration name:(_) @n) @c",
+      "(
+          ((class_declaration name:(_) @z) @qc)
+          (#eq? @z \"@n\")
+          )",
+    )],
+  );
+
+  let source_code = "class Test {
+      pub void foobar(int a, int b, int c){
+        boolean isFlagTreated = true;
+        isFlagTreated = false;
+        if (isFlagTreated) {
+          System.out.println(a + b + c);
+        }
+      }
+    }";
+
+  let mut rule_store =
+    RuleStore::dummy_with_scope(vec![scope_generator_method, scope_generator_class]);
+  let mut parser = get_parser(String::from("java"));
+
+  let source_code_unit = SourceCodeUnit::new(
+    &mut parser,
+    source_code.to_string(),
+    &HashMap::new(),
+    PathBuf::new().as_path(),
+  );
+
+  let scope_query_method = ScopeGenerator::get_scope_query(
+    source_code_unit.clone(),
+    "Method",
+    133,
+    134,
+    &mut rule_store,
+  );
+
+  assert!(eq_without_whitespace(
+    scope_query_method.as_str(),
+    "(((method_declaration 
+      name : (_) @z
+            parameters : (formal_parameters
+                (formal_parameter type:(_) @r0)
+                    (formal_parameter type:(_) @r1)
+                    (formal_parameter type:(_) @r2))) @qd)
+            (#eq? @z \"foobar\")
+            (#eq? @r0 \"int\")
+            (#eq? @r1 \"int\")
+            (#eq? @r2 \"int\")
+            )"
+  ));
+
+  let scope_query_class =
+    ScopeGenerator::get_scope_query(source_code_unit.clone(), "Class", 133, 134, &mut rule_store);
+  assert!(eq_without_whitespace(
+    scope_query_class.as_str(),
+    "(
+        ((class_declaration name:(_) @z) @qc)
+        (#eq? @z \"Test\")
+        )"
+  ));
+}
+
+/// Negative test for the generated scope query, given scope generators, source code and position of pervious edit.
+#[test]
+#[should_panic]
+fn test_get_scope_query_negative() {
+  let scope_generator_method = ScopeGenerator::new(
+    "Method",
+    vec![ScopeQueryGenerator::new(
+      "((method_declaration 
+          name : (_) @n
+                parameters : (formal_parameters
+                    (formal_parameter type:(_) @t0)
+                        (formal_parameter type:(_) @t1)
+                        (formal_parameter type:(_) @t2))) @xd2)",
+      "(((method_declaration 
+                        name : (_) @z
+                              parameters : (formal_parameters
+                                  (formal_parameter type:(_) @r0)
+                                      (formal_parameter type:(_) @r1)
+                                      (formal_parameter type:(_) @r2))) @qd)
+                  (#eq? @z \"@n\")
+                  (#eq? @r0 \"@t0\")
+                  (#eq? @r1 \"@t1\")
+                  (#eq? @r2 \"@t2\")
+                  )",
+    )],
+  );
+
+  let scope_generator_class = ScopeGenerator::new(
+    "Class",
+    vec![ScopeQueryGenerator::new(
+      "(class_declaration name:(_) @n) @c",
+      "(
+          ((class_declaration name:(_) @z) @qc)
+          (#eq? @z \"@n\")
+          )",
+    )],
+  );
+
+  let source_code = "class Test {
+      pub void foobar(int a, int b, int c, int d){
+        boolean isFlagTreated = true;
+        isFlagTreated = false;
+        if (isFlagTreated) {
+          System.out.println(a + b + c + d);
+        }
+      }
+    }";
+
+  let mut rule_store =
+    RuleStore::dummy_with_scope(vec![scope_generator_method, scope_generator_class]);
+  let mut parser = get_parser(String::from("java"));
+
+  let source_code_unit = SourceCodeUnit::new(
+    &mut parser,
+    source_code.to_string(),
+    &HashMap::new(),
+    PathBuf::new().as_path(),
+  );
+
+  let _ = ScopeGenerator::get_scope_query(
+    source_code_unit.clone(),
+    "Method",
+    133,
+    134,
+    &mut rule_store,
+  );
+}
diff --git a/generic/piranha/src/models/unit_tests/source_code_unit_test.rs b/generic/piranha/src/models/unit_tests/source_code_unit_test.rs
new file mode 100644
index 000000000..37957b97d
--- /dev/null
+++ b/generic/piranha/src/models/unit_tests/source_code_unit_test.rs
@@ -0,0 +1,100 @@
+use {
+  super::SourceCodeUnit,
+  crate::{
+    models::edit::Edit, models::rule::Rule, utilities::eq_without_whitespace,
+    utilities::tree_sitter_utilities::get_parser,
+  },
+  std::{collections::HashMap, path::PathBuf},
+  tree_sitter::Range,
+};
+
+/// Positive test of an edit being applied  given replacement range  and replacement string.
+#[test]
+fn test_apply_edit_positive() {
+  let source_code = "class Test {
+      pub void foobar(){
+        boolean isFlagTreated = true;
+        isFlagTreated = true;
+        if (isFlagTreated) {
+          // Do something;
+        }
+      }
+    }";
+
+  let mut parser = get_parser(String::from("java"));
+
+  let mut source_code_unit = SourceCodeUnit::new(
+    &mut parser,
+    source_code.to_string(),
+    &HashMap::new(),
+    PathBuf::new().as_path(),
+  );
+
+  let _ = source_code_unit.apply_edit(
+    &Edit::new(
+      Range {
+        start_byte: 46,
+        end_byte: 75,
+        start_point: tree_sitter::Point { row: 3, column: 9 },
+        end_point: tree_sitter::Point { row: 3, column: 38 },
+      },
+      String::new(),
+      Rule::dummy(),
+      HashMap::new(),
+    ),
+    &mut parser,
+  );
+
+  println!("{}", source_code_unit.code());
+  assert!(eq_without_whitespace(
+    r#"class Test {
+      pub void foobar(){
+        
+        isFlagTreated = true;
+        if (isFlagTreated) {
+          // Do something;
+        }
+      }
+    }"#,
+    &source_code_unit.code()
+  ));
+}
+
+/// Negative test of an edit being applied given invalid replacement range and replacement string.
+#[test]
+#[should_panic(expected = "byte index 1000 is out of bounds")]
+fn test_apply_edit_negative() {
+  let source_code = "class Test {
+      pub void foobar(){
+        boolean isFlagTreated = true;
+        isFlagTreated = false;
+        if (isFlagTreated) {
+          // Do something;
+        }
+      }
+    }";
+
+  let mut parser = get_parser(String::from("java"));
+
+  let mut source_code_unit = SourceCodeUnit::new(
+    &mut parser,
+    source_code.to_string(),
+    &HashMap::new(),
+    PathBuf::new().as_path(),
+  );
+
+  let _ = source_code_unit.apply_edit(
+    &Edit::new(
+      Range {
+        start_byte: 1000,
+        end_byte: 10075,
+        start_point: tree_sitter::Point { row: 3, column: 9 },
+        end_point: tree_sitter::Point { row: 3, column: 38 },
+      },
+      String::new(),
+      Rule::dummy(),
+      HashMap::new(),
+    ),
+    &mut parser,
+  );
+}
diff --git a/generic/piranha/src/test/mod.rs b/generic/piranha/src/tests/mod.rs
similarity index 86%
rename from generic/piranha/src/test/mod.rs
rename to generic/piranha/src/tests/mod.rs
index f040b148c..14e5bf3da 100644
--- a/generic/piranha/src/test/mod.rs
+++ b/generic/piranha/src/tests/mod.rs
@@ -17,18 +17,19 @@ use colored::Colorize;
 use log::info;
 
 use crate::config::CommandLineArguments;
-use crate::piranha::flag_cleaner::FlagCleaner;
-use crate::piranha::piranha_arguments::PiranhaArguments;
-use crate::piranha::source_code_unit::SourceCodeUnit;
+use crate::models::piranha_arguments::PiranhaArguments;
+use crate::models::source_code_unit::SourceCodeUnit;
+use crate::piranha::{execute_piranha};
 use crate::utilities::{eq_without_whitespace, find_file, initialize_logger, read_file};
 
-pub mod test_piranha_java;
+mod test_piranha_java;
+mod unit_tests;
 
 use std::sync::Once;
 
 static INIT: Once = Once::new();
 
-pub fn initialize() {
+fn initialize() {
   INIT.call_once(|| {
     initialize_logger(true);
   });
@@ -74,9 +75,3 @@ fn check_result(updated_files: Vec<SourceCodeUnit>, path_to_expected: PathBuf) {
   }
   assert!(all_files_match);
 }
-
-fn execute_piranha(args: PiranhaArguments) -> Vec<SourceCodeUnit> {
-  let mut flag_cleaner = FlagCleaner::new(args);
-  flag_cleaner.perform_cleanup();
-  flag_cleaner.get_updated_files()
-}
diff --git a/generic/piranha/src/test/test_piranha_java.rs b/generic/piranha/src/tests/test_piranha_java.rs
similarity index 100%
rename from generic/piranha/src/test/test_piranha_java.rs
rename to generic/piranha/src/tests/test_piranha_java.rs
diff --git a/generic/piranha/src/piranha/mod.rs b/generic/piranha/src/tests/unit_tests.rs
similarity index 86%
rename from generic/piranha/src/piranha/mod.rs
rename to generic/piranha/src/tests/unit_tests.rs
index ba6ee8c4b..06b640da0 100644
--- a/generic/piranha/src/piranha/mod.rs
+++ b/generic/piranha/src/tests/unit_tests.rs
@@ -11,7 +11,9 @@ Copyright (c) 2022 Uber Technologies, Inc.
  limitations under the License.
 */
 
-pub mod flag_cleaner;
-pub mod piranha_arguments;
-pub mod rule_store;
-pub mod source_code_unit;
+use super::initialize;
+
+#[test]
+fn test_rule_update() {
+  initialize();
+}
diff --git a/generic/piranha/src/utilities/unit_tests/tree_sitter_utilities_test.rs b/generic/piranha/src/utilities/unit_tests/tree_sitter_utilities_test.rs
new file mode 100644
index 000000000..4fbdda984
--- /dev/null
+++ b/generic/piranha/src/utilities/unit_tests/tree_sitter_utilities_test.rs
@@ -0,0 +1,250 @@
+use std::{collections::HashMap, path::PathBuf};
+
+use tree_sitter::Query;
+
+use {
+  super::{get_parser, substitute_tags, PiranhaHelpers, TreeSitterHelpers},
+  crate::models::{
+    constraint::Constraint, rule::Rule, rule_store::RuleStore, source_code_unit::SourceCodeUnit,
+  },
+};
+
+#[test]
+fn test_get_all_matches_for_query_positive() {
+  let source_code = r#"
+      class Test {
+        void foobar(Experiment exp) {
+          if (exp.isFlagTreated(SOME_FLAG)){
+            //Do Something
+          }
+          // Do something else
+          
+          if (abc && exp.isFlagTreated(SOME_FLAG)){
+            // Do this too!
+          }
+        }
+      }
+    "#;
+  let language_name = String::from("java");
+  let query = Query::new(
+    language_name.get_language(),
+    r#"((
+        (method_invocation 
+          name : (_) @name
+          arguments: ((argument_list 
+                          ([
+                            (field_access field: (_)@argument)
+                            (_) @argument
+                           ])) )
+              
+       ) @method_invocation
+      )
+      (#eq? @name "isFlagTreated")
+      (#eq? @argument "SOME_FLAG")
+      )"#,
+  )
+  .unwrap();
+
+  let mut parser = get_parser(String::from("java"));
+  let ast = parser
+    .parse(&source_code, None)
+    .expect("Could not parse code");
+  let node = ast.root_node();
+
+  let matches = node.get_all_matches_for_query(
+    source_code.to_string(),
+    &query,
+    true,
+    Some("method_invocation".to_string()),
+  );
+  assert_eq!(matches.len(), 2);
+}
+
+#[test]
+fn test_get_all_matches_for_query_negative() {
+  let source_code = r#"
+      class Test {
+        void foobar(Experiment exp) {
+          if (exp.isFlagTreated(SOME_OTHER)){
+            //Do Something
+          }
+          // Do something else
+          
+          if (abc && exp.isFlagTreated(SOME_OTHER)){
+            // Do this too!
+          }
+        }
+      }
+    "#;
+  let language_name = String::from("java");
+  let query = Query::new(
+    language_name.get_language(),
+    r#"((
+        (method_invocation 
+          name : (_) @name
+          arguments: ((argument_list 
+                          ([
+                            (field_access field: (_)@argument)
+                            (_) @argument
+                           ])) )
+              
+       ) @method_invocation
+      )
+      (#eq? @name "isFlagTreated")
+      (#eq? @argument "SOME_FLAG")
+      )"#,
+  )
+  .unwrap();
+
+  let mut parser = get_parser(String::from("java"));
+  let ast = parser
+    .parse(&source_code, None)
+    .expect("Could not parse code");
+  let node = ast.root_node();
+
+  let matches = node.get_all_matches_for_query(
+    source_code.to_string(),
+    &query,
+    true,
+    Some("method_invocation".to_string()),
+  );
+  assert!(matches.is_empty());
+}
+
+#[test]
+fn test_satisfies_constraints_positive() {
+  let rule = Rule::new(
+    "test",
+    "(
+      ((local_variable_declaration
+                      declarator: (variable_declarator
+                                          name: (_) @variable_name
+                                          value: [(true) (false)] @init)) @variable_declaration)
+      )",
+    "variable_declaration",
+    "",
+    None,
+    Some(vec![Constraint::new(
+      String::from("(method_declaration) @md"),
+      vec![String::from(
+        "(
+         ((assignment_expression
+                         left: (_) @a.lhs
+                         right: (_) @a.rhs) @assignment)
+         (#eq? @a.lhs \"@variable_name\")
+         (#not-eq? @a.rhs \"@init\")
+       )",
+      )],
+    )]),
+  );
+  let source_code = "class Test {
+      pub void foobar(){
+        boolean isFlagTreated = true;
+        isFlagTreated = true;
+        if (isFlagTreated) {
+        // Do something;
+        }
+       }
+      }";
+
+  let mut rule_store = RuleStore::dummy();
+
+  let mut parser = get_parser(String::from("java"));
+
+  let source_code_unit = SourceCodeUnit::new(
+    &mut parser,
+    source_code.to_string(),
+    &HashMap::new(),
+    PathBuf::new().as_path(),
+  );
+
+  let node = &source_code_unit
+    .root_node()
+    .descendant_for_byte_range(50, 72)
+    .unwrap();
+
+  assert!(node.satisfies_constraint(
+    source_code_unit.clone(),
+    &rule,
+    &HashMap::from([
+      ("variable_name".to_string(), "isFlagTreated".to_string()),
+      ("init".to_string(), "true".to_string())
+    ]),
+    &mut rule_store,
+  ));
+}
+
+#[test]
+fn test_satisfies_constraints_negative() {
+  let rule = Rule::new(
+    "test",
+    "(
+      ((local_variable_declaration
+                      declarator: (variable_declarator
+                                          name: (_) @variable_name
+                                          value: [(true) (false)] @init)) @variable_declaration)
+      )",
+    "variable_declaration",
+    "",
+    None,
+    Some(vec![Constraint::new(
+      String::from("(method_declaration) @md"),
+      vec![String::from(
+        "(
+         ((assignment_expression
+                         left: (_) @a.lhs
+                         right: (_) @a.rhs) @assignment)
+         (#eq? @a.lhs \"@variable_name\")
+         (#not-eq? @a.rhs \"@init\")
+       )",
+      )],
+    )]),
+  );
+  let source_code = "class Test {
+      pub void foobar(){
+        boolean isFlagTreated = true;
+        isFlagTreated = false;
+        if (isFlagTreated) {
+        // Do something;
+        }
+       }
+      }";
+
+  let mut rule_store = RuleStore::dummy();
+
+  let mut parser = get_parser(String::from("java"));
+
+  let source_code_unit = SourceCodeUnit::new(
+    &mut parser,
+    source_code.to_string(),
+    &HashMap::new(),
+    PathBuf::new().as_path(),
+  );
+
+  let node = &source_code_unit
+    .root_node()
+    .descendant_for_byte_range(50, 72)
+    .unwrap();
+
+  assert!(!node.satisfies_constraint(
+    source_code_unit.clone(),
+    &rule,
+    &HashMap::from([
+      ("variable_name".to_string(), "isFlagTreated".to_string()),
+      ("init".to_string(), "true".to_string())
+    ]),
+    &mut rule_store,
+  ));
+}
+
+#[test]
+fn test_substitute_tags() {
+  let substitutions = HashMap::from([
+    ("variable_name".to_string(), "isFlagTreated".to_string()),
+    ("init".to_string(), "true".to_string()),
+  ]);
+  assert_eq!(
+    substitute_tags("@variable_name foo bar @init".to_string(), &substitutions),
+    "isFlagTreated foo bar true"
+  )
+}
diff --git a/generic/piranha/src/utilities/unit_tests/utilities_test.rs b/generic/piranha/src/utilities/unit_tests/utilities_test.rs
new file mode 100644
index 000000000..c82566ffd
--- /dev/null
+++ b/generic/piranha/src/utilities/unit_tests/utilities_test.rs
@@ -0,0 +1,55 @@
+use crate::utilities::find_file;
+use serde_derive::Deserialize;
+use std::path::PathBuf;
+
+use super::{read_file, read_toml};
+
+#[derive(Deserialize, Default)]
+struct TestStruct {
+  name: String,
+}
+
+#[test]
+fn test_read_file() {
+  let project_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+  let path_to_test_file = project_root.join("test-resources/utility_tests/sample.toml");
+  let result = read_file(&path_to_test_file);
+  assert!(result.is_ok());
+  let content = result.ok().unwrap();
+  assert!(!content.is_empty());
+  assert!(content.eq(r#"name = 'Piranha'"#));
+}
+
+#[test]
+fn test_read_toml() {
+  let project_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+  let path_to_test_file = project_root.join("test-resources/utility_tests/sample.toml");
+  let result: TestStruct = read_toml(&path_to_test_file, false);
+  assert!(result.name.eq("Piranha"));
+}
+
+#[test]
+fn test_read_toml_default() {
+  let project_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+  let path_to_test_file =
+    project_root.join("test-resources/utility_tests/another_sample.toml.toml");
+  let result: TestStruct = read_toml(&path_to_test_file, true);
+  assert!(result.name.eq(""));
+}
+
+#[test]
+fn test_find_file_positive() {
+  let project_root =
+    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-resources/utility_tests/");
+  let f = find_file(&project_root, "sample.toml");
+  assert!(f.is_file());
+}
+
+#[test]
+#[should_panic]
+fn test_find_file_negative() {
+  let project_root =
+    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-resources/utility_tests/");
+  let f = find_file(&project_root, "another_sample.toml.toml");
+  assert!(f.is_file());
+}
diff --git a/generic/piranha/test-resources/utility_tests/sample.toml b/generic/piranha/test-resources/utility_tests/sample.toml
new file mode 100644
index 000000000..01bae4fe4
--- /dev/null
+++ b/generic/piranha/test-resources/utility_tests/sample.toml
@@ -0,0 +1,1 @@
+name = 'Piranha'
\ No newline at end of file