diff --git a/demo/custom_cleanups.ipynb b/demo/custom_cleanups.ipynb new file mode 100644 index 000000000..6de2f1789 --- /dev/null +++ b/demo/custom_cleanups.ipynb @@ -0,0 +1,53 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Custom Cleanups \n", + "\n", + "This is a guide for developing automation for ***custom code cleanups***.\n", + "\n", + "Languages supported : Java, Kotlin, Go, Python, Swift, and TypeScript\n", + "\n", + "ℹ️ **_NOTE:_** This tutorial is in Python for pedagogical purposes. \n", + "Piranha can be used with the CLI or using the Rust API. \n", + "We would encourage following this tutorial even if you do not intend to use the Python API.\n", + "\n", + "✅ **Prerequisite**: [Stale FF cleanup tutorial](./stale_feature_flag_cleanup_tutorial.ipynb)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".env", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10.9" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "8940432f2365cfc2130f5c62b266313bc73e38258967dc33b60edac5998749e5" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/demo/stale_feature_flag_cleanup_demos.py b/demo/stale_feature_flag_cleanup_demos.py deleted file mode 100644 index 42df76c1c..000000000 --- a/demo/stale_feature_flag_cleanup_demos.py +++ /dev/null @@ -1,82 +0,0 @@ -from os.path import join, dirname, getmtime, exists -from polyglot_piranha import execute_piranha, PiranhaArguments -import logging -from logging import info - -feature_flag_dir = join(dirname(__file__), "feature_flag_cleanup") - - -def run_java_ff_demo(): - info("Running the stale feature flag cleanup demo for Java") - - directory_path = join(feature_flag_dir, "java") - modified_file_path = join(directory_path, "SampleClass.java") - deleted_join_path = join(directory_path, "TestEnum.java") - configuration_path = join(directory_path, "configurations") - - old_mtime = getmtime(modified_file_path) - - args = PiranhaArguments( - "java", - { - "stale_flag_name": "SAMPLE_STALE_FLAG", - "treated": "true", - "treated_complement": "false", - }, - path_to_codebase=directory_path, - path_to_configurations=configuration_path, - ) - output_summary_java = execute_piranha(args) - - assert len(output_summary_java) == 2 - - for summary in output_summary_java: - assert len(summary.rewrites) > 0 - - new_mtime = getmtime(modified_file_path) - - assert old_mtime < new_mtime - assert not exists(deleted_join_path) - - -def run_kt_ff_demo(): - info("Running the stale feature flag cleanup demo for Kotlin") - - directory_path = join(feature_flag_dir, "kt") - modified_file_path = join(directory_path, "SampleClass.kt") - deleted_join_path = join(directory_path, "TestEnum.kt") - configuration_path = join(directory_path, "configurations") - - old_mtime = getmtime(modified_file_path) - - args = PiranhaArguments( - "kt", - { - "stale_flag_name": "SAMPLE_STALE_FLAG", - "treated": "true", - "treated_complement": "false", - }, - path_to_codebase=directory_path, - path_to_configurations=configuration_path, - ) - - output_summary_kt = execute_piranha(args) - - assert len(output_summary_kt) == 2 - - for summary in output_summary_kt: - assert len(summary.rewrites) > 0 - - new_mtime = getmtime(modified_file_path) - - assert old_mtime < new_mtime - assert not exists(deleted_join_path) - - -FORMAT = "%(levelname)s %(name)s %(asctime)-15s %(filename)s:%(lineno)d %(message)s" -logging.basicConfig(format=FORMAT) -logging.getLogger().setLevel(logging.DEBUG) - -run_java_ff_demo() -run_kt_ff_demo() -print("Completed running the stale feature flag cleanup demos") diff --git a/demo/stale_feature_flag_cleanup_tutorial.ipynb b/demo/stale_feature_flag_cleanup_tutorial.ipynb new file mode 100644 index 000000000..6d9e1237b --- /dev/null +++ b/demo/stale_feature_flag_cleanup_tutorial.ipynb @@ -0,0 +1,262 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Cleaning up code related to stale feature flags\n", + "\n", + "This is a guide for developing automation for ***cleaning up code related to stale feature flags*** \n", + "\n", + "Languages supported : Java, Kotlin, Go and Swift\n", + "\n", + "\n", + "ℹ️ **_NOTE:_** This tutorial is in Python for pedagogical purposes. \n", + "Piranha can be used with the CLI or using the Rust API. \n", + "We would encourage following this tutorial even if you do not intend to use the Python API.\n", + "\n", + "Now let's set up the notebook by installing the polyglot-piranha from source. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Please install [rust](https://rustup.rs/)\n", + "! python3 -m venv ../.env\n", + "! cd ../ && cargo build --no-default-features && maturin develop\n", + "\n", + "from polyglot_piranha import PiranhaArguments, execute_piranha, RuleGraph, Rule, OutgoingEdges" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "tags": [] + }, + "source": [ + "
\n", + " CLI\n", + "

Build the executable from source

\n", + " \n", + " cargo build --no-default-features -r\n", + " \n", + "

The executable polyglot_piranha will be available under target/release

\n", + "
\n", + "\n", + "Now let's take the below example where we want to clean up the code related to the stale flag `SOME_STALE_FLAG` 🏴 because it has been permanently enabled." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "input_code = \"\"\"package com.uber.piranha;\n", + "class SampleJava {\n", + " \n", + " // An enum that declares the feature flag \n", + " enum FeatureFlag {\n", + " SOME_STALE_FLAG,\n", + " }\n", + "\n", + " public void sampleMethod(ExperimentInterface exp) {\n", + " if (exp.isToggleEnabled(SOME_STALE_FLAG)) {\n", + " System.out.println(\"SOME_STALE_FLAG is enabled\");\n", + " } else {\n", + " System.out.println(\"SOME_STALE_FLAG is disabled\");\n", + " }\n", + " }\n", + "}\n", + "\"\"\"" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + " CLI\n", + "

A similar example for CLI is available here

\n", + " \n", + " cargo build --no-default-features -r\n", + " \n", + "

The executable polyglot_piranha will be available under target/release

\n", + "
\n", + "\n", + "\n", + "We will now craft a Piranha rule to update the feature flag API `exp.isToggleEnabled(SOME_STALE_FLAG)` and delete the enum case `SOME_STALE_FLAG`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# a rule to update `exp.isToggleEnabled(SOME_STALE_FLAG)`\n", + "update_rule_toggle_enabled = Rule (\n", + " name= \"update_toggle_enabled\",\n", + " query= \"\"\"((\n", + " (method_invocation \n", + " name : (_) @name\n", + " arguments: ((argument_list \n", + " ([\n", + " (field_access field: (_)@argument)\n", + " (_) @argument\n", + " ])) )\n", + " ) @method_invocation)\n", + " (#eq? @name \"isToggleEnabled\")\n", + " (#eq? @argument \"@stale_flag_name\")\n", + " )\"\"\",\n", + " replace_node=\"method_invocation\",\n", + " replace=\"@is_treated\",\n", + " holes= set([\"stale_flag_name\", \"is_treated\"]),\n", + " # Try commenting out the below line and see the difference in output\n", + " groups= set([\"replace_expression_with_boolean_literal\"])\n", + ")\n", + "\n", + "# a rule to delete enum case `SOME_STALE_FLAG`\n", + "delete_enum_case = Rule (\n", + " name = \"delete_enum_constant\",\n", + " query = \"\"\"(\n", + " ((enum_constant name : (_) @enum_case) @ec) \n", + " (#eq? @enum_case \"@stale_flag_name\")\n", + " )\"\"\",\n", + " replace_node = \"ec\",\n", + " replace = \"\",\n", + " holes = set([\"stale_flag_name\"]),\n", + " # Try commenting out the below line and see the difference in output\n", + " groups = set([\"delete_enum_entry\"]),\n", + ")\n", + "\n", + "rule_graph= RuleGraph(rules=[update_rule_toggle_enabled, delete_enum_case], edges=[])\n", + "\n", + "piranha_args = PiranhaArguments(\n", + " language = \"java\",\n", + " code_snippet= input_code,\n", + " rule_graph= rule_graph,\n", + " substitutions= {'stale_flag_name': 'SOME_STALE_FLAG', 'is_treated': 'true'},\n", + " dry_run= True,\n", + " cleanup_comments = True\n", + ")\n", + "output = execute_piranha(piranha_args)\n", + "output_content= output[0].content\n", + "assert output_content != input_code # sanity test for this tutorial\n", + "print(output_content)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The rule `update_rule_toggle_enabled` specifies a rule that matches against an expression like `exp.isTreated(SOME_STALE_FLAG)` and replaces it with `true`. \n", + " \n", + "
\n", + " See the rule\n", + "\n", + " ##### `update_rule_toggle_enabled`\n", + " ```python Rule (\n", + " name= \"update_toggle_enabled\",\n", + " query= \"\"\"((\n", + " (method_invocation \n", + " name : (_) @name\n", + " arguments: ((argument_list \n", + " ([\n", + " (field_access field: (_)@argument)\n", + " (_) @argument\n", + " ])) )\n", + " ) @method_invocation)\n", + " (#eq? @name \"isToggleEnabled\")\n", + " (#eq? @argument \"@stale_flag_name\")\n", + " )\"\"\",\n", + " replace_node=\"method_invocation\",\n", + " replace=\"@is_treated\",\n", + " holes= set([\"stale_flag_name\", \"is_treated\"])\n", + " # Try un-commenting the below line and see the difference in output\n", + " groups= set([\"replace_expression_with_boolean_literal\"])\n", + ")\n", + " ```\n", + "
\n", + "\n", + "\n", + "The **`query`** property of the rule is a [tree-sitter query](https://tree-sitter.github.io/tree-sitter/using-parsers#pattern-matching-with-queries) that is matched against the source code to identify specific AST patterns.\n", + "\n", + "ℹ️ **_NOTE:_** Use [tree-sitter playground](https://tree-sitter.github.io/tree-sitter/playground) to craft the query that matches your specific APIs. This playground allows a user to create a query and match it against some input source code. This playground \"syntax highlights\" in the input code snippet based on input query. \n", + "
\n", + "Using the tree-sitter play ground locally \n", + "Install tree-sitter [CLI](https://github.com/tree-sitter/tree-sitter/blob/master/cli/README.md).\n", + "```bash\n", + "git clone https://github.com/tree-sitter/tree-sitter-java.git # Or any other tree-sitter language repo\n", + "cd tree-sitter-java\n", + "tree-sitter generate \n", + "tree-sitter build-wasm\n", + "tree-sitter playground\n", + "```\n", + "
\n", + "\n", + "The node captured by the tag-name specified in the **`replace_node`** property is replaced with the pattern specified in the **`replace`** property.\n", + "The **`replace`** pattern can use the tags from the **`query`** to construct a replacement based on the match (somewhat similar to [regex-replace](https://docs.microsoft.com/en-us/visualstudio/ide/using-regular-expressions-in-visual-studio?view=vs-2022)).\n", + "\n", + "Each rule also contains the **`groups`** property, that specifies the kind of change performed by this rule. Based on this group, appropriate\n", + "cleanup will be performed by Piranha. For instance, `replace_expression_with_boolean_literal` will trigger deep cleanups to eliminate dead code (like eliminating `consequent` of a `if statement`) caused by replacing an expression with a boolean literal.\n", + "Currently, Piranha provides deep clean-ups for edits that belong the groups - `replace_expression_with_boolean_literal`, `delete_enum_entry`, and `delete_method`. Adding an appropriate entry to the groups, hooks up the user defined rules to the pre-built cleanup rules.\n", + "\n", + "The property **`holes`** specify the holes or template variables `@treated` and `@stale_flag_name` that need to be replaced with some concrete values so that the rule matches only the feature flag API usages corresponding to a specific flag, and replace it specifically with `true` or `false`.\n", + "\n", + "The values for these template variables is specified via the `substitutions` dictionary. This dictionary is required to construct piranha arguments. \n", + "\n", + "
\n", + " CLI\n", + "

Java

\n", + " \n", + " ./target/release/polyglot_piranha -c demo/feature_flag_cleanup/java -f demo/feature_flag_cleanup/java/configurations -l \"java\" -s stale_flag_name=SAMPLE_STALE_FLAG -s treated=true -s treated_complement=false\n", + " \n", + " \n", + "

Kotlin

\n", + " \n", + " ./target/release/polyglot_piranha -c demo/feature_flag_cleanup/kt -f demo/feature_flag_cleanup/kt/configurations -l \"kt\" -s stale_flag_name=SAMPLE_STALE_FLAG -s treated=true -s treated_complement=false\n", + " \n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + }, + "vscode": { + "interpreter": { + "hash": "8940432f2365cfc2130f5c62b266313bc73e38258967dc33b60edac5998749e5" + } + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/polyglot_piranha.pyi b/polyglot_piranha.pyi index 0b043413d..c1bbd5990 100644 --- a/polyglot_piranha.pyi +++ b/polyglot_piranha.pyi @@ -38,12 +38,12 @@ class PiranhaArguments: rule_graph: Optional[RuleGraph]= None, path_to_codebase: Optional[str] = None, code_snippet: Optional[str] = None, - dry_run: Optional[bool] = None, - cleanup_comments: Optional[bool] = None, - cleanup_comments_buffer: Optional[int] = None, - number_of_ancestors_in_parent_scope: Optional[int] = None, - delete_file_if_empty : Optional[bool] = None, - delete_consecutive_new_lines : Optional[bool] = None, + dry_run: bool = False, + cleanup_comments: bool = False, + cleanup_comments_buffer: int = 2, + number_of_ancestors_in_parent_scope: int = None, + delete_file_if_empty : bool = True, + delete_consecutive_new_lines : bool = False, path_to_output: Optional[str] = None ):