Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New codemod to add missing self/cls #355

Merged
merged 8 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions integration_tests/test_fix_missing_self_or_cls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from codemodder.codemods.test import (
BaseIntegrationTest,
original_and_expected_from_code_path,
)
from core_codemods.fix_missing_self_or_cls import (
FixMissingSelfOrCls,
FixMissingSelfOrClsTransformer,
)


class TestFixMissingSelfOrCls(BaseIntegrationTest):
codemod = FixMissingSelfOrCls
code_path = "tests/samples/fix_missing_self_or_cls.py"
original_code, expected_new_code = original_and_expected_from_code_path(
code_path,
[
(
1,
""" def instance_method(self):\n""",
),
(
5,
""" def class_method(cls):\n""",
),
],
)

# fmt: off
expected_diff = (
"""--- \n"""
"""+++ \n"""
"""@@ -1,7 +1,7 @@\n"""
""" class MyClass:\n"""
"""- def instance_method():\n"""
"""+ def instance_method(self):\n"""
""" print("instance_method")\n"""
""" \n"""
""" @classmethod\n"""
"""- def class_method():\n"""
"""+ def class_method(cls):\n"""
""" print("class_method")\n"""
)
# fmt: on

expected_line_change = "2"
change_description = FixMissingSelfOrClsTransformer.change_description
num_changes = 2
45 changes: 45 additions & 0 deletions integration_tests/test_sonar_fix_missing_self_or_cls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from codemodder.codemods.test import (
BaseIntegrationTest,
original_and_expected_from_code_path,
)
from core_codemods.fix_missing_self_or_cls import FixMissingSelfOrClsTransformer
from core_codemods.sonar.sonar_fix_missing_self_or_cls import SonarFixMissingSelfOrCls


class TestSonarFixMissingSelfOrCls(BaseIntegrationTest):
codemod = SonarFixMissingSelfOrCls
code_path = "tests/samples/fix_missing_self_or_cls.py"
original_code, expected_new_code = original_and_expected_from_code_path(
code_path,
[
(
1,
""" def instance_method(self):\n""",
),
(
5,
""" def class_method(cls):\n""",
),
],
)
sonar_issues_json = "tests/samples/sonar_issues.json"
# fmt: off
expected_diff = (
"""--- \n"""
"""+++ \n"""
"""@@ -1,7 +1,7 @@\n"""
""" class MyClass:\n"""
"""- def instance_method():\n"""
"""+ def instance_method(self):\n"""
""" print("instance_method")\n"""
""" \n"""
""" @classmethod\n"""
"""- def class_method():\n"""
"""+ def class_method(cls):\n"""
""" print("class_method")\n"""
)
# fmt: on

expected_line_change = "2"
change_description = FixMissingSelfOrClsTransformer.change_description
num_changes = 2
16 changes: 14 additions & 2 deletions src/codemodder/codemods/utils_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,18 @@ def is_builtin_function(self, node: cst.Call):
return matchers.matches(node.func, matchers.Name())
return False

def is_staticmethod(self, node: cst.FunctionDef) -> bool:
for decorator in node.decorators:
if self.find_base_name(decorator.decorator) == "builtins.staticmethod":
return True
return False

def is_classmethod(self, node: cst.FunctionDef) -> bool:
for decorator in node.decorators:
if self.find_base_name(decorator.decorator) == "builtins.classmethod":
return True
return False

def find_accesses(self, node) -> Collection[Access]:
if scope := self.get_metadata(ScopeProvider, node, None):
return scope.accesses[node]
Expand Down Expand Up @@ -437,10 +449,10 @@ def find_immediate_class_def(self, node: cst.CSTNode) -> Optional[cst.ClassDef]:

def path_to_root(self, node: cst.CSTNode) -> list[cst.CSTNode]:
"""
Returns node's path to root. Includes self.
Returns node's path to `node` (excludes `node`).
"""
path = []
maybe_parent = node
maybe_parent = self.get_parent(node)
while maybe_parent:
path.append(maybe_parent)
maybe_parent = self.get_parent(maybe_parent)
Expand Down
9 changes: 9 additions & 0 deletions src/codemodder/scripts/generate_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,10 @@ class DocMetadata:
importance="Medium",
guidance_explained="This change is safe and will prevent runtime `ValueError`.",
),
"fix-missing-self-or-cls": DocMetadata(
importance="Medium",
guidance_explained="This change is safe and will prevent errors when calling on these instance or class methods..",
),
}

METADATA = CORE_METADATA | {
Expand Down Expand Up @@ -305,6 +309,11 @@ class DocMetadata:
guidance_explained=CORE_METADATA["jwt-decode-verify"].guidance_explained,
need_sarif="Yes (Sonar)",
),
"fix-missing-self-or-cls-S5719": DocMetadata(
importance=CORE_METADATA["fix-missing-self-or-cls"].importance,
guidance_explained=CORE_METADATA["fix-missing-self-or-cls"].guidance_explained,
need_sarif="Yes (Sonar)",
),
}


Expand Down
4 changes: 4 additions & 0 deletions src/core_codemods/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from .fix_deprecated_logging_warn import FixDeprecatedLoggingWarn
from .fix_empty_sequence_comparison import FixEmptySequenceComparison
from .fix_hasattr_call import TransformFixHasattrCall
from .fix_missing_self_or_cls import FixMissingSelfOrCls
from .fix_mutable_params import FixMutableParams
from .flask_enable_csrf_protection import FlaskEnableCSRFProtection
from .flask_json_response_type import FlaskJsonResponseType
Expand Down Expand Up @@ -48,6 +49,7 @@
from .sonar.sonar_django_receiver_on_top import SonarDjangoReceiverOnTop
from .sonar.sonar_exception_without_raise import SonarExceptionWithoutRaise
from .sonar.sonar_fix_assert_tuple import SonarFixAssertTuple
from .sonar.sonar_fix_missing_self_or_cls import SonarFixMissingSelfOrCls
from .sonar.sonar_flask_json_response_type import SonarFlaskJsonResponseType
from .sonar.sonar_jwt_decode_verify import SonarJwtDecodeVerify
from .sonar.sonar_literal_or_new_object_identity import SonarLiteralOrNewObjectIdentity
Expand Down Expand Up @@ -127,6 +129,7 @@
DjangoModelWithoutDunderStr,
TransformFixHasattrCall,
FixDataclassDefaults,
FixMissingSelfOrCls,
],
)

Expand All @@ -142,5 +145,6 @@
SonarFlaskJsonResponseType,
SonarDjangoJsonResponseType,
SonarJwtDecodeVerify,
SonarFixMissingSelfOrCls,
],
)
15 changes: 15 additions & 0 deletions src/core_codemods/docs/pixee_python_fix-missing-self-or-cls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Python instance methods must be defined with `self` as the first argument. Likewise, class methods must have `cls` as the first argument. This codemod will add these arguments when the method/class method has no arguments defined.

Our changes look something like this:

```diff
class MyClass:
- def instance_method():
+ def instance_method(self):
print("instance_method")

@classmethod
- def class_method():
+ def class_method(cls):
print("class_method")
```
87 changes: 87 additions & 0 deletions src/core_codemods/fix_missing_self_or_cls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import libcst as cst

from codemodder.codemods.libcst_transformer import (
LibcstResultTransformer,
LibcstTransformerPipeline,
)
from codemodder.codemods.utils_mixin import NameAndAncestorResolutionMixin
from core_codemods.api import Metadata, ReviewGuidance
from core_codemods.api.core_codemod import CoreCodemod


class FixMissingSelfOrClsTransformer(
LibcstResultTransformer, NameAndAncestorResolutionMixin
):
change_description = "Add `self` or `cls` parameter to instance or class method."

def leave_FunctionDef(
self, original_node: cst.FunctionDef, updated_node: cst.FunctionDef
) -> cst.FunctionDef:
# TODO: add filter by include or exclude that works for nodes
# that that have different start/end numbers.

if not self.find_immediate_class_def(original_node):
# If `original_node` is not inside a class, nothing to do.
return original_node

if self.find_immediate_function_def(original_node):
# If `original_node` is inside a class but also nested within a function/method
# We won't touch it.
return original_node

if original_node.decorators:
if self.is_staticmethod(original_node):
return updated_node
if self.is_classmethod(original_node):
if self.has_no_args(original_node):
self.report_change(original_node)
return updated_node.with_changes(
params=updated_node.params.with_changes(
params=[cst.Param(name=cst.Name("cls"))]
)
)
else:
if self.has_no_args(original_node):
self.report_change(original_node)
return updated_node.with_changes(
params=updated_node.params.with_changes(
params=[cst.Param(name=self._pick_arg_name(original_node))]
)
)
return updated_node

def _pick_arg_name(self, node: cst.FunctionDef) -> cst.Name:
match node.name:
case cst.Name(value="__new__") | cst.Name(value="__init_subclass__"):
new_name = "cls"
case _:
new_name = "self"
return cst.Name(value=new_name)

def has_no_args(self, node: cst.FunctionDef) -> bool:
converted_star_arg = (
None
if node.params.star_arg is cst.MaybeSentinel.DEFAULT
else node.params.star_arg
)
return not any(
(
node.params.params,
converted_star_arg,
node.params.kwonly_params,
node.params.star_kwarg,
node.params.posonly_params,
)
)


FixMissingSelfOrCls = CoreCodemod(
metadata=Metadata(
name="fix-missing-self-or-cls",
review_guidance=ReviewGuidance.MERGE_AFTER_CURSORY_REVIEW,
summary="Add Missing Positional Parameter for Instance and Class Methods",
references=[],
),
transformer=LibcstTransformerPipeline(FixMissingSelfOrClsTransformer),
detector=None,
)
12 changes: 12 additions & 0 deletions src/core_codemods/sonar/sonar_fix_missing_self_or_cls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from codemodder.codemods.base_codemod import Reference
from codemodder.codemods.sonar import SonarCodemod
from core_codemods.fix_missing_self_or_cls import FixMissingSelfOrCls

SonarFixMissingSelfOrCls = SonarCodemod.from_core_codemod(
name="fix-missing-self-or-cls-S5719",
other=FixMissingSelfOrCls,
rules=["python:S5719"],
new_references=[
Reference(url="https://rules.sonarsource.com/python/RSPEC-5719/"),
],
)
Loading
Loading