{
 "cells": [
  {
   "cell_type": "raw",
   "metadata": {
    "raw_mimetype": "text/restructuredtext"
   },
   "source": [
    "=================\n",
    "Build a Lint Rule\n",
    "=================\n",
    "\n",
    "Example: NoInheritFromObjectRule\n",
    "================================\n",
    "In Python 3, a class is inherited from ``object`` by default.\n",
    "Explicitly inheriting from ``object`` is redundant, so removing it keeps the code simpler.\n",
    "In this tutorial, we'd like to build a lint rule to identify cases when\n",
    "a class inherit from object and add an autofix to remove it."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "nbsphinx": "hidden"
   },
   "outputs": [],
   "source": [
    "import sys\n",
    "sys.path.append(\"../../\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# an example with unnecessary object inheritance\n",
    "class C(object):\n",
    "    ...\n",
    "\n",
    "\n",
    "# the above example can be simplified as this\n",
    "class C:\n",
    "    ..."
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {
    "raw_mimetype": "text/restructuredtext"
   },
   "source": [
    "Pick a Good Name\n",
    "================\n",
    "Before starting creating a new lint rule, let's figure out a good short name for it.\n",
    "**A good lint rule name should be short and actionable.**\n",
    "Instead of describing the issue, describe the action needs to be taken to fix it.\n",
    "So developers can easily learn how to fix the issue by just reading the name.\n",
    "A lint rule name is a class name in camel case and ends with ``Rule``.\n",
    "\n",
    "For example, to suggest gather await calls in a loop, it's better to name as ``GatherSequentialAwaitRule``\n",
    "instead of ``AwaitInLoopLintRule`` (less actionable).\n",
    "If the action need is to remove/cleanup something, it can be named as ``No...Rule``, e.g. ``NoAssertEqualsRule``.\n",
    "\n",
    "In this example, we name the rule as NoInheritFromObjectRule.\n",
    "\n",
    "Lint Rule Scaffolding\n",
    "=====================\n",
    "A lint rule is a subclass of :class:`~fixit.CstLintRule` which inherits from `CSTVisitor` in LibCST.\n",
    "LibCST provides `visitors <https://libcst.readthedocs.io/en/latest/tutorial.html#Build-Visitor-or-Transformer>`_ for traversing the syntax tree.\n",
    "Defining a ``visit_`` or ``leave_`` functions for a specific types of `CSTNode <https://libcst.readthedocs.io/en/latest/nodes.html>`_ allows us to register a callback function to be called during the syntax tree traversal.\n",
    "\n",
    "In this example, we can inspect all class definitions in a file by defining a ``visit_ClassDef`` function, which will get called once per class definition encountered during syntax tree traversal.\n",
    "\n",
    "To add types, we'll need to import `ClassDef <https://libcst.readthedocs.io/en/latest/nodes.html#libcst.ClassDef>`_ from libcst, and annotate the function signature:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from fixit import CstLintRule\n",
    "import libcst as cst\n",
    "\n",
    "\n",
    "class NoInheritFromObjectRule(CstLintRule):\n",
    "    def visit_ClassDef(self, node: cst.ClassDef) -> None:\n",
    "        ..."
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {
    "raw_mimetype": "text/restructuredtext"
   },
   "source": [
    "We don't need to perform any work after visiting our children, so we won't define ``leave_ClassDef``.\n",
    "While not needed for this lint rule, if we wanted to visit specific attributes of a given node type,\n",
    "we could specify that attribute as part of the method name too:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def visit_If(self, node: cst.If) -> None:\n",
    "    # called first\n",
    "    ...\n",
    "\n",
    "\n",
    "def visit_If_test(self, node: cst.If) -> None:\n",
    "    # called after visit_If, but before we visit the test attribute\n",
    "    # `leave_If_test` would be called next, followed by `leave_If`.\n",
    "    if check_something(node.test):\n",
    "        ..."
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {
    "raw_mimetype": "text/restructuredtext"
   },
   "source": [
    "Iteration order of attributes is the same as the order they appear in the source code.\n",
    "In this case, that means visit_If_test is called before visit_If_body and visit_If_orelse.\n",
    "\n",
    "Use fixit's cli to generate a skeleton of adding a new rule file::\n",
    "\n",
    "    $ python -m fixit.cli.add_new_rule # Creates new.py at fixit/rules/new.py\n",
    "    $ python -m fixit.cli.add_new_rule --path fixit/rules/my_rule.py --name rule_name # Creates my_rule.py at path specified \n",
    "\n",
    "This will generate a rule file used to create and add new rule to fixit module. \n",
    "\n",
    "The `fixit.cli.add_new_rule` contains two argument, ``-path`` and ``--name``\n",
    "\n",
    "- ``--path`` is used to create rule file at path given in ``--path``, defaults to ``fixit/rules/new.py``\n",
    "- ``--name`` is used to assign the name of the rule and should be in snake case, defaults to the rule file name if ``path`` provided else `new`. Otherwise, considers the value specified in ``--name``\n",
    "\n",
    "Please provide the name of the rule in snake case without the suffix ``rule`` as CLI will take care of adding ``Rule`` to end of the rule name. \n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "nbsphinx": "hidden"
   },
   "outputs": [],
   "source": [
    "import os\n",
    "os.chdir(\"../../\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "! python -m fixit.cli.add_new_rule --path fixit/rules/my_rule.py --name abcd"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "! cat fixit/rules/my_rule.py"
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {
    "raw_mimetype": "text/restructuredtext"
   },
   "source": [
    "Now, you can add rule's functionality on top of above generated file. Also, make sure to check the class name in generated file. Your class name should always be suffixed with ``Rule``.\n",
    "\n",
    "The Declarative Matcher API\n",
    "===========================\n",
    "\n",
    "Once we have a ``ClassDef`` node, we need to see if it contains a base class named ``object``.\n",
    "We could implement by inspecting attributes of the node using equality and isinstance."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# check if any of the base classes of this class def is \"object\"\n",
    "def visit_ClassDef(self, node: cst.ClassDef):\n",
    "    has_object_base = any(\n",
    "        isinstance(arg.value, cst.Name) and arg.value.value == \"object\"\n",
    "        for arg in node.bases\n",
    "    )"
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {
    "raw_mimetype": "text/restructuredtext"
   },
   "source": [
    "Unfortunately, that imperative approach isn't easy to read or write, especially when matching a more complex syntax tree structure.\n",
    "LibCST has `a declarative matcher API <https://libcst.readthedocs.io/en/latest/matchers_tutorial.html>`_\n",
    "which allows you to define the shape of an object to match. It's like a regular expression, but for the CST."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import libcst.matchers as m\n",
    "\n",
    "\n",
    "def visit_ClassDef(self, node: cst.ClassDef):\n",
    "    has_object_base = m.matches(\n",
    "        node, m.ClassDef(bases=[m.AtLeastN(n=1, matcher=m.Arg(value=m.Name(\"object\")))])\n",
    "    )"
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {
    "raw_mimetype": "text/restructuredtext"
   },
   "source": [
    "It makes the code easier to read and maintain.\n",
    "\n",
    "Reporting Violations\n",
    "====================\n",
    "To report a lint violation, simply call :func:`~fixit.CstLintRule.report` with a CSTNode.\n",
    "Define a lint message via the ``MESSAGE`` attribute in your lint class.\n",
    "Keep your lint descriptions brief but informative. Link to other documentation if you want to provide an extended explanation. Feedback that you provide to a developer should be clear and actionable.\n",
    "Add a docstring to the rule class to provide more context and the docstring will be included in the generated document."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class NoInheritFromObjectRule(CstLintRule):\n",
    "    \"\"\"\n",
    "    In Python 3, a class is inherited from ``object`` by default.\n",
    "    Explicitly inheriting from ``object`` is redundant, so removing it keeps the code simpler.\n",
    "    \"\"\"\n",
    "    MESSAGE = \"Inheriting from object is a no-op. 'class Foo:' is just fine =)\"\n",
    "\n",
    "    def visit_ClassDef(self, node: cst.ClassDef) -> None:\n",
    "        new_bases = tuple(\n",
    "            base for base in node.bases if not m.matches(base.value, m.Name(\"object\"))\n",
    "        )\n",
    "        if tuple(node.bases) != new_bases:\n",
    "            self.report(node)"
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {
    "raw_mimetype": "text/restructuredtext"
   },
   "source": [
    "Adding an Autofix\n",
    "=================\n",
    "\n",
    "Warning a user about a problem is nice, but offering to fix it for them is even better!\n",
    "That's what you can do with an auto-fixer.\n",
    "Currently we support replacing a node (use `with_changes <https://libcst.readthedocs.io/en/latest/nodes.html#libcst.CSTNode.with_changes>`_ to modify a CSTNode) or removing it (by passing a `libcst.RemovalSentinel <https://libcst.readthedocs.io/en/latest/visitors.html?highlight=RemovalSentinel#libcst.RemovalSentinel>`_ as the replacement).\n",
    "In our example, we want to remove any references to object in the base classes of a class definition:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class NoInheritFromObjectRule(CstLintRule):\n",
    "    MESSAGE = \"Inheriting from object is a no-op. 'class Foo:' is just fine =)\"\n",
    "\n",
    "    def visit_ClassDef(self, node: cst.ClassDef) -> None:\n",
    "        new_bases = tuple(\n",
    "            base for base in node.bases if not m.matches(base.value, m.Name(\"object\"))\n",
    "        )\n",
    "\n",
    "        if tuple(node.bases) != new_bases:\n",
    "            # reconstruct classdef, removing parens if bases and keywords are empty\n",
    "            new_classdef = node.with_changes(\n",
    "                bases=new_bases,\n",
    "                lpar=cst.MaybeSentinel.DEFAULT,\n",
    "                rpar=cst.MaybeSentinel.DEFAULT,\n",
    "            )\n",
    "\n",
    "            # report warning and autofix\n",
    "            self.report(node, replacement=new_classdef)"
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {
    "raw_mimetype": "text/restructuredtext"
   },
   "source": [
    "This example also makes use of `libcst.MaybeSentinel <https://libcst.readthedocs.io/en/latest/nodes.html?highlight=MaybeSentinel#libcst.MaybeSentinel>`_ to properly handle the rendering syntax.\n",
    "In this case, using a MaybeSentinel for the parens fields will remove the parentheses following the class name if removing object from bases means there are no other base classes or keywords in the class definition.\n",
    "\n",
    "\n",
    "Skipping Files\n",
    "==============\n",
    "Certain behaviors may be acceptable in a set of files, but not in others.\n",
    "We can avoid running the linter on some files by overriding should_skip_file.\n",
    "The properties provided by ``self.context`` (:func:`~fixit.common.base.BaseContext`) are useful when implementing :func:`~fixit.base.common.CstLintRule.should_skip_file`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MyRule(CstLintRule):\n",
    "    def should_skip_file(self):\n",
    "        # Assert statements are okay for tests.\n",
    "        # We could check the self.context.file_path object (see pathlib.Path), but\n",
    "        # Context has a helper property for tests, since this is a common use-case\n",
    "        return self.context.in_tests"
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {
    "raw_mimetype": "text/restructuredtext"
   },
   "source": [
    "Your lint rule does something, but now you need to test it.\n",
    "Testing lint rules is easy, and every lint rule should include test cases.\n",
    "Continue to the next tutorial :doc:`test_a_lint_rule`\n",
    "\n",
    "\n",
    "Reference\n",
    "=========\n",
    "\n",
    ".. autoclass:: fixit.CstLintRule\n",
    ".. autoclass:: fixit.common.base.BaseContext\n",
    ".. autoclass:: fixit.CstContext"
   ]
  }
 ],
 "metadata": {
  "celltoolbar": "Edit Metadata",
  "kernelspec": {
   "display_name": "Python 3",
   "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.8.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
