diff --git a/integration_tests/sonar/test_sonar_django_jmodel_without_dunder_str.py b/integration_tests/sonar/test_sonar_django_jmodel_without_dunder_str.py new file mode 100644 index 00000000..fdd1fe76 --- /dev/null +++ b/integration_tests/sonar/test_sonar_django_jmodel_without_dunder_str.py @@ -0,0 +1,42 @@ +from codemodder.codemods.test import SonarIntegrationTest +from core_codemods.django_model_without_dunder_str import ( + DjangoModelWithoutDunderStrTransformer, +) +from core_codemods.sonar.sonar_django_model_without_dunder_str import ( + SonarDjangoModelWithoutDunderStr, +) + + +class TestSonarDjangoModelWithoutDunderStr(SonarIntegrationTest): + codemod = SonarDjangoModelWithoutDunderStr + code_path = "tests/samples/django_model.py" + replacement_lines = [ + (15, """\n"""), + (16, """ def __str__(self):\n"""), + (17, """ model_name = self.__class__.__name__\n"""), + ( + 18, + """ fields_str = ", ".join((f"{field.name}={getattr(self, field.name)}" for field in self._meta.fields))\n""", + ), + (19, """ return f"{model_name}({fields_str})"\n"""), + ] + + # fmt: off + expected_diff = ( + """--- \n""" + """+++ \n""" + """@@ -12,3 +12,8 @@\n""" + """ phone = models.IntegerField(blank=True)\n""" + """ class Meta:\n""" + """ app_label = 'myapp'\n""" + """+\n""" + """+ def __str__(self):\n""" + """+ model_name = self.__class__.__name__\n""" + """+ fields_str = ", ".join((f"{field.name}={getattr(self, field.name)}" for field in self._meta.fields))\n""" + """+ return f"{model_name}({fields_str})"\n""" + ) + # fmt: on + + expected_line_change = "10" + change_description = DjangoModelWithoutDunderStrTransformer.change_description + num_changed_files = 1 diff --git a/src/codemodder/scripts/generate_docs.py b/src/codemodder/scripts/generate_docs.py index e2cf5dc1..a56a6bbc 100644 --- a/src/codemodder/scripts/generate_docs.py +++ b/src/codemodder/scripts/generate_docs.py @@ -295,6 +295,7 @@ class DocMetadata: "fix-float-equality-S1244", "fix-math-isclose-S6727", "sql-parameterization-S3649", + "django-model-without-dunder-str-S6554", ] SONAR_CODEMODS = { name: DocMetadata( diff --git a/src/core_codemods/__init__.py b/src/core_codemods/__init__.py index 10047fbf..f83938b1 100644 --- a/src/core_codemods/__init__.py +++ b/src/core_codemods/__init__.py @@ -52,6 +52,9 @@ from .secure_flask_session_config import SecureFlaskSessionConfig from .secure_random import SecureRandom from .sonar.sonar_django_json_response_type import SonarDjangoJsonResponseType +from .sonar.sonar_django_model_without_dunder_str import ( + SonarDjangoModelWithoutDunderStr, +) from .sonar.sonar_django_receiver_on_top import SonarDjangoReceiverOnTop from .sonar.sonar_enable_jinja2_autoescape import SonarEnableJinja2Autoescape from .sonar.sonar_exception_without_raise import SonarExceptionWithoutRaise @@ -168,6 +171,7 @@ SonarFixFloatEquality, SonarFixMathIsClose, SonarSQLParameterization, + SonarDjangoModelWithoutDunderStr, ], ) diff --git a/src/core_codemods/sonar/sonar_django_model_without_dunder_str.py b/src/core_codemods/sonar/sonar_django_model_without_dunder_str.py new file mode 100644 index 00000000..23cd2a5b --- /dev/null +++ b/src/core_codemods/sonar/sonar_django_model_without_dunder_str.py @@ -0,0 +1,10 @@ +from core_codemods.django_model_without_dunder_str import DjangoModelWithoutDunderStr +from core_codemods.sonar.api import SonarCodemod + +SonarDjangoModelWithoutDunderStr = SonarCodemod.from_core_codemod( + name="django-model-without-dunder-str-S6554", + other=DjangoModelWithoutDunderStr, + rule_id="python:S6554", + rule_name='Django models should define a "__str__" method', + rule_url="https://rules.sonarsource.com/python/RSPEC-6554/", +) diff --git a/tests/codemods/sonar/test_sonar_django_model_without_dunder_str.py b/tests/codemods/sonar/test_sonar_django_model_without_dunder_str.py new file mode 100644 index 00000000..582472a1 --- /dev/null +++ b/tests/codemods/sonar/test_sonar_django_model_without_dunder_str.py @@ -0,0 +1,52 @@ +import json + +from codemodder.codemods.test import BaseSASTCodemodTest +from core_codemods.sonar.sonar_django_model_without_dunder_str import ( + SonarDjangoModelWithoutDunderStr, +) + + +class TestSonarDjangoModelWithoutDunderStr(BaseSASTCodemodTest): + codemod = SonarDjangoModelWithoutDunderStr + tool = "sonar" + + def test_name(self): + assert self.codemod.name == "django-model-without-dunder-str-S6554" + assert self.codemod.id == "sonar:python/django-model-without-dunder-str-S6554" + + def test_simple(self, tmpdir): + input_code = """ + from django.db import models + + class User(models.Model): + name = models.CharField(max_length=100) + phone = models.IntegerField(blank=True) + """ + expected = """ + from django.db import models + + class User(models.Model): + name = models.CharField(max_length=100) + phone = models.IntegerField(blank=True) + + def __str__(self): + model_name = self.__class__.__name__ + fields_str = ", ".join((f"{field.name}={getattr(self, field.name)}" for field in self._meta.fields)) + return f"{model_name}({fields_str})" + """ + issues = { + "issues": [ + { + "rule": "python:S6554", + "status": "OPEN", + "component": "code.py", + "textRange": { + "startLine": 3, + "endLine": 3, + "startOffset": 6, + "endOffset": 10, + }, + } + ] + } + self.run_and_assert(tmpdir, input_code, expected, results=json.dumps(issues)) diff --git a/tests/samples/django_model.py b/tests/samples/django_model.py new file mode 100644 index 00000000..9cd1adc5 --- /dev/null +++ b/tests/samples/django_model.py @@ -0,0 +1,14 @@ +import django +from django.conf import settings +from django.db import models + +# required to run this module standalone for testing +settings.configure() +django.setup() + + +class User(models.Model): + name = models.CharField(max_length=100) + phone = models.IntegerField(blank=True) + class Meta: + app_label = 'myapp' diff --git a/tests/samples/sonar_issues.json b/tests/samples/sonar_issues.json index 5f254323..02a57d12 100644 --- a/tests/samples/sonar_issues.json +++ b/tests/samples/sonar_issues.json @@ -1,5 +1,5 @@ { - "total": 41, + "total": 42, "p": 1, "ps": 500, "paging": { @@ -1862,6 +1862,41 @@ "severity": "HIGH" } ] + }, + { + "key": "AY8LdruyywzNF5GhDiHJ", + "rule": "python:S6554", + "severity": "MAJOR", + "component": "pixee_codemodder-python:django_model.py", + "project": "pixee_codemodder-python", + "line": 4, + "hash": "8eca309fd79127651d55034d0f9981ea", + "textRange": { + "startLine": 10, + "endLine": 10, + "startOffset": 6, + "endOffset": 10 + }, + "flows": [], + "status": "OPEN", + "message": "Define a \"__str__\" method for this Django model.", + "effort": "10min", + "debt": "10min", + "assignee": "clavedeluna@github", + "author": "danalitovsky+git@gmail.com", + "tags": [], + "creationDate": "2024-04-23T16:56:52+0200", + "updateDate": "2024-04-23T16:57:31+0200", + "type": "CODE_SMELL", + "organization": "pixee", + "cleanCodeAttribute": "COMPLETE", + "cleanCodeAttributeCategory": "INTENTIONAL", + "impacts": [ + { + "softwareQuality": "MAINTAINABILITY", + "severity": "MEDIUM" + } + ] } ], "components": [