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

Converted Remaining Codemods to Sonar Codemods #239

Merged
merged 3 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 5 additions & 2 deletions integration_tests/test_django_json_response_type.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from core_codemods.django_json_response_type import DjangoJsonResponseType
from core_codemods.django_json_response_type import (
DjangoJsonResponseType,
DjangoJsonResponseTypeTransformer,
)
from integration_tests.base_test import (
BaseIntegrationTest,
original_and_expected_from_code_path,
Expand Down Expand Up @@ -32,5 +35,5 @@ class TestDjangoJsonResponseType(BaseIntegrationTest):
# fmt: on

expected_line_change = "6"
change_description = DjangoJsonResponseType.change_description
change_description = DjangoJsonResponseTypeTransformer.change_description
num_changed_files = 1
7 changes: 5 additions & 2 deletions integration_tests/test_flask_json_response_type.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from core_codemods.flask_json_response_type import FlaskJsonResponseType
from core_codemods.flask_json_response_type import (
FlaskJsonResponseType,
FlaskJsonResponseTypeTransformer,
)
from integration_tests.base_test import (
BaseIntegrationTest,
original_and_expected_from_code_path,
Expand Down Expand Up @@ -32,5 +35,5 @@ class TestFlaskJsonResponseType(BaseIntegrationTest):
# fmt: on

expected_line_change = "9"
change_description = FlaskJsonResponseType.change_description
change_description = FlaskJsonResponseTypeTransformer.change_description
num_changed_files = 1
40 changes: 40 additions & 0 deletions integration_tests/test_sonar_django_json_response_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from core_codemods.django_json_response_type import DjangoJsonResponseTypeTransformer
from core_codemods.sonar.sonar_django_json_response_type import (
SonarDjangoJsonResponseType,
)
from integration_tests.base_test import (
BaseIntegrationTest,
original_and_expected_from_code_path,
)


class TestSonarDjangoJsonResponseType(BaseIntegrationTest):
codemod = SonarDjangoJsonResponseType
code_path = "tests/samples/django_json_response_type.py"
original_code, expected_new_code = original_and_expected_from_code_path(
code_path,
[
(
5,
""" return HttpResponse(json_response, content_type="application/json")\n""",
),
],
)
sonar_issues_json = "tests/samples/sonar_issues.json"

# fmt: off
expected_diff =(
"""--- \n"""
"""+++ \n"""
"""@@ -3,4 +3,4 @@\n"""
""" \n"""
""" def foo(request):\n"""
""" json_response = json.dumps({ "user_input": request.GET.get("input") })\n"""
"""- return HttpResponse(json_response)\n"""
"""+ return HttpResponse(json_response, content_type="application/json")\n"""
)
# fmt: on

expected_line_change = "6"
change_description = DjangoJsonResponseTypeTransformer.change_description
num_changed_files = 1
40 changes: 40 additions & 0 deletions integration_tests/test_sonar_flask_json_response_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from core_codemods.flask_json_response_type import FlaskJsonResponseTypeTransformer
from core_codemods.sonar.sonar_flask_json_response_type import (
SonarFlaskJsonResponseType,
)
from integration_tests.base_test import (
BaseIntegrationTest,
original_and_expected_from_code_path,
)


class TestSonarFlaskJsonResponseType(BaseIntegrationTest):
codemod = SonarFlaskJsonResponseType
code_path = "tests/samples/flask_json_response_type.py"
original_code, expected_new_code = original_and_expected_from_code_path(
code_path,
[
(
8,
""" return make_response(json_response, {'Content-Type': 'application/json'})\n""",
),
],
)
sonar_issues_json = "tests/samples/sonar_issues.json"

# fmt: off
expected_diff =(
"""--- \n"""
"""+++ \n"""
"""@@ -6,4 +6,4 @@\n"""
""" @app.route("/test")\n"""
""" def foo(request):\n"""
""" json_response = json.dumps({ "user_input": request.GET.get("input") })\n"""
"""- return make_response(json_response)\n"""
"""+ return make_response(json_response, {'Content-Type': 'application/json'})\n"""
)
# fmt: on

expected_line_change = "9"
change_description = FlaskJsonResponseTypeTransformer.change_description
num_changed_files = 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from core_codemods.remove_assertion_in_pytest_raises import (
RemoveAssertionInPytestRaisesTransformer,
)
from core_codemods.sonar.sonar_remove_assertion_in_pytest_raises import (
SonarRemoveAssertionInPytestRaises,
)
from integration_tests.base_test import (
BaseIntegrationTest,
original_and_expected_from_code_path,
)


class TestSonarRemoveAssertionInPytestRaises(BaseIntegrationTest):
codemod = SonarRemoveAssertionInPytestRaises
code_path = "tests/samples/remove_assertion_in_pytest_raises.py"
original_code, expected_new_code = original_and_expected_from_code_path(
code_path,
[
(5, """ assert 1\n"""),
(6, """ assert 2\n"""),
],
)
sonar_issues_json = "tests/samples/sonar_issues.json"

# fmt: off
expected_diff =(
"""--- \n"""
"""+++ \n"""
"""@@ -3,5 +3,5 @@\n"""
""" def test_foo():\n"""
""" with pytest.raises(ZeroDivisionError):\n"""
""" error = 1/0\n"""
"""- assert 1\n"""
"""- assert 2\n"""
"""+ assert 1\n"""
"""+ assert 2\n"""
)
# fmt: on

expected_line_change = "4"
change_description = RemoveAssertionInPytestRaisesTransformer.change_description
num_changed_files = 1
num_changes = 1
42 changes: 30 additions & 12 deletions src/codemodder/codemods/base_visitor.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
from typing import Any, Tuple
from libcst import MetadataDependent
from libcst.codemod import ContextAwareVisitor, VisitorBasedCodemodCommand
from libcst.metadata import PositionProvider

from codemodder.result import Result


# TODO: this should just be part of BaseTransformer and BaseVisitor?
class UtilsMixin:
results: list[Result] | None
class UtilsMixin(MetadataDependent):
METADATA_DEPENDENCIES: Tuple[Any, ...] = (PositionProvider,)

def __init__(self, results: list[Result] | None):
def __init__(
self,
results: list[Result] | None,
line_exclude: list[int],
line_include: list[int],
): # pylint: disable=super-init-not-called
self.results = results
self.line_exclude = line_exclude
self.line_include = line_include

def filter_by_result(self, node):
pos_to_match = self.node_position(node)
Expand All @@ -37,26 +45,36 @@ def node_is_selected(self, node) -> bool:

def node_position(self, node):
# See https://github.com/Instagram/LibCST/blob/main/libcst/_metadata_dependent.py#L112
return self.get_metadata(self.METADATA_DEPENDENCIES[0], node)
return self.get_metadata(PositionProvider, node)

def lineno_for_node(self, node):
return self.node_position(node).start.line


class BaseTransformer(VisitorBasedCodemodCommand, UtilsMixin):
METADATA_DEPENDENCIES: Tuple[Any, ...] = (PositionProvider,)

def __init__(self, context, results: list[Result] | None):
super().__init__(context)
UtilsMixin.__init__(self, results)
def __init__(
self,
context,
results: list[Result] | None,
line_include: list[int],
line_exclude: list[int],
):
VisitorBasedCodemodCommand.__init__(self, context)
UtilsMixin.__init__(self, results, line_exclude, line_include)


class BaseVisitor(ContextAwareVisitor, UtilsMixin):
METADATA_DEPENDENCIES: Tuple[Any, ...] = (PositionProvider,)

def __init__(self, context, results: list[Result]):
super().__init__(context)
UtilsMixin.__init__(self, results)
def __init__(
self,
context,
results: list[Result] | None,
line_include: list[int],
line_exclude: list[int],
):
ContextAwareVisitor.__init__(self, context)
UtilsMixin.__init__(self, results, line_exclude, line_include)


def match_line(pos, line):
Expand Down
15 changes: 6 additions & 9 deletions src/codemodder/codemods/libcst_transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ def __init__(
):
del _transformer

super().__init__(context, results)
super().__init__(
context,
results,
line_include=file_context.line_include,
line_exclude=file_context.line_exclude,
)
self.file_context = file_context

@classmethod
Expand Down Expand Up @@ -119,14 +124,6 @@ def add_change_from_position(
def lineno_for_node(self, node):
return self.node_position(node).start.line

@property
def line_exclude(self):
return self.file_context.line_exclude

@property
def line_include(self):
return self.file_context.line_include

def add_dependency(self, dependency: Dependency):
self.file_context.add_dependency(dependency)

Expand Down
19 changes: 19 additions & 0 deletions src/codemodder/scripts/generate_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,25 @@ class DocMetadata:
guidance_explained=CORE_METADATA["fix-assert-tuple"].guidance_explained,
need_sarif="Yes (Sonar)",
),
"remove-assertion-in-pytest-raises-S5915": DocMetadata(
importance=CORE_METADATA["remove-assertion-in-pytest-raises"].importance,
guidance_explained=CORE_METADATA[
"remove-assertion-in-pytest-raises"
].guidance_explained,
need_sarif="Yes (Sonar)",
),
"flask-json-response-type-S5131": DocMetadata(
importance=CORE_METADATA["flask-json-response-type"].importance,
guidance_explained=CORE_METADATA["flask-json-response-type"].guidance_explained,
need_sarif="Yes (Sonar)",
),
"django-json-response-type-S5131": DocMetadata(
importance=CORE_METADATA["django-json-response-type"].importance,
guidance_explained=CORE_METADATA[
"django-json-response-type"
].guidance_explained,
need_sarif="Yes (Sonar)",
),
}


Expand Down
8 changes: 8 additions & 0 deletions src/core_codemods/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@
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_remove_assertion_in_pytest_raises import (
SonarRemoveAssertionInPytestRaises,
)
from .sonar.sonar_flask_json_response_type import SonarFlaskJsonResponseType
from .sonar.sonar_django_json_response_type import SonarDjangoJsonResponseType

registry = CodemodCollection(
origin="pixee",
Expand Down Expand Up @@ -123,5 +128,8 @@
SonarDjangoReceiverOnTop,
SonarExceptionWithoutRaise,
SonarFixAssertTuple,
SonarRemoveAssertionInPytestRaises,
SonarFlaskJsonResponseType,
SonarDjangoJsonResponseType,
],
)
53 changes: 38 additions & 15 deletions src/core_codemods/django_json_response_type.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,18 @@
import libcst as cst
from codemodder.codemods.libcst_transformer import (
LibcstResultTransformer,
LibcstTransformerPipeline,
)
from codemodder.codemods.semgrep import SemgrepRuleDetector
from core_codemods.api import (
Metadata,
Reference,
ReviewGuidance,
SimpleCodemod,
)
from core_codemods.api.core_codemod import CoreCodemod


class DjangoJsonResponseType(SimpleCodemod):
metadata = Metadata(
name="django-json-response-type",
summary="Set content type to `application/json` for `django.http.HttpResponse` with JSON data",
review_guidance=ReviewGuidance.MERGE_WITHOUT_REVIEW,
references=[
Reference(
url="https://docs.djangoproject.com/en/4.0/ref/request-response/#django.http.HttpResponse.__init__"
),
Reference(
url="https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#output-encoding-for-javascript-contexts"
),
],
)
class DjangoJsonResponseTypeTransformer(LibcstResultTransformer):
change_description = "Sets `content_type` to `application/json`."
detector_pattern = """
rules:
Expand Down Expand Up @@ -49,3 +41,34 @@ def on_result_found(self, _, updated_node):
),
],
)


semgrep_rule = """
rules:
- id: django-json-response-type
mode: taint
pattern-sources:
- pattern: json.dumps(...)
pattern-sinks:
- patterns:
- pattern: django.http.HttpResponse(...)
- pattern-not: django.http.HttpResponse(...,content_type=...,...)
"""

DjangoJsonResponseType = CoreCodemod(
metadata=Metadata(
name="django-json-response-type",
summary="Set content type to `application/json` for `django.http.HttpResponse` with JSON data",
review_guidance=ReviewGuidance.MERGE_WITHOUT_REVIEW,
references=[
Reference(
url="https://docs.djangoproject.com/en/4.0/ref/request-response/#django.http.HttpResponse.__init__"
),
Reference(
url="https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#output-encoding-for-javascript-contexts"
),
],
),
transformer=LibcstTransformerPipeline(DjangoJsonResponseTypeTransformer),
detector=SemgrepRuleDetector(rule=semgrep_rule),
)
Loading
Loading