Skip to content

Commit

Permalink
Sonar version of fix-assert-tuple
Browse files Browse the repository at this point in the history
  • Loading branch information
andrecsilva committed Feb 1, 2024
1 parent 5143749 commit 322399f
Show file tree
Hide file tree
Showing 12 changed files with 141 additions and 42 deletions.
4 changes: 2 additions & 2 deletions integration_tests/test_fix_assert_tuple.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from core_codemods.fix_assert_tuple import FixAssertTuple
from core_codemods.fix_assert_tuple import FixAssertTuple, FixAssertTupleTransform
from integration_tests.base_test import (
BaseIntegrationTest,
original_and_expected_from_code_path,
Expand All @@ -13,5 +13,5 @@ class TestFixAssertTuple(BaseIntegrationTest):
)
expected_diff = "--- \n+++ \n@@ -1 +1,2 @@\n-assert (1 == 1, 2 == 2)\n+assert 1 == 1\n+assert 2 == 2\n"
expected_line_change = "1"
change_description = FixAssertTuple.change_description
change_description = FixAssertTupleTransform.change_description
num_changes = 2
19 changes: 19 additions & 0 deletions integration_tests/test_sonar_fix_assert_tuple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from core_codemods.fix_assert_tuple import FixAssertTupleTransform
from core_codemods.sonar.sonar_fix_assert_tuple import SonarFixAssertTuple
from integration_tests.base_test import (
BaseIntegrationTest,
original_and_expected_from_code_path,
)


class TestFixAssertTuple(BaseIntegrationTest):
codemod = SonarFixAssertTuple
code_path = "tests/samples/fix_assert_tuple.py"
original_code, expected_new_code = original_and_expected_from_code_path(
code_path, [(0, "assert 1 == 1\n"), (1, "assert 2 == 2\n")]
)
expected_diff = "--- \n+++ \n@@ -1 +1,2 @@\n-assert (1 == 1, 2 == 2)\n+assert 1 == 1\n+assert 2 == 2\n"
sonar_issues_json = "tests/samples/sonar_issues.json"
expected_line_change = "1"
change_description = FixAssertTupleTransform.change_description
num_changes = 2
13 changes: 5 additions & 8 deletions src/codemodder/codemods/base_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,11 @@ class UtilsMixin:
def __init__(self, results: list[Result] | None):
self.results = results

def filter_by_result(self, pos_to_match):
def filter_by_result(self, node):
pos_to_match = self.node_position(node)
if self.results is None:
return True
return any(
location.match(pos_to_match)
for result in self.results
for location in result.locations
)
return any(result.match_location(pos_to_match, node) for result in self.results)

def filter_by_path_includes_or_excludes(self, pos_to_match):
"""
Expand All @@ -34,9 +31,9 @@ def filter_by_path_includes_or_excludes(self, pos_to_match):

def node_is_selected(self, node) -> bool:
pos_to_match = self.node_position(node)
return self.filter_by_result(
return self.filter_by_result(node) and self.filter_by_path_includes_or_excludes(
pos_to_match
) and self.filter_by_path_includes_or_excludes(pos_to_match)
)

def node_position(self, node):
# See https://github.com/Instagram/LibCST/blob/main/libcst/_metadata_dependent.py#L112
Expand Down
21 changes: 11 additions & 10 deletions src/codemodder/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,23 @@ class Location(ABCDataclass):
start: LineInfo
end: LineInfo

def match(self, pos):
start_column = self.start.column
end_column = self.end.column
return (
pos.start.line == self.start.line
and (pos.start.column in (start_column - 1, start_column))
and pos.end.line == self.end.line
and (pos.end.column in (end_column - 1, end_column))
)


@dataclass
class Result(ABCDataclass):
rule_id: str
locations: list[Location]

def match_location(self, pos, node): # pylint: disable=unused-argument
for location in self.locations:
start_column = location.start.column
end_column = location.end.column
return (
pos.start.line == location.start.line
and (pos.start.column in (start_column - 1, start_column))
and pos.end.line == location.end.line
and (pos.end.column in (end_column - 1, end_column))
)


class ResultSet(dict[str, dict[Path, list[Result]]]):
def add_result(self, result: Result):
Expand Down
5 changes: 5 additions & 0 deletions src/codemodder/scripts/generate_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,11 @@ class DocMetadata:
guidance_explained=CORE_METADATA["exception-without-raise"].guidance_explained,
need_sarif="Yes (Sonar)",
),
"fix-assert-tuple-S5905": DocMetadata(
importance=CORE_METADATA["fix-assert-tuple"].importance,
guidance_explained=CORE_METADATA["fix-assert-tuple"].guidance_explained,
need_sarif="Yes (Sonar)",
),
}


Expand Down
15 changes: 15 additions & 0 deletions src/codemodder/sonar_results.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import libcst as cst
import json
from pathlib import Path
from typing_extensions import Self

from codemodder.result import LineInfo, Location, Result, ResultSet
from codemodder.logging import logger
from dataclasses import replace


class SonarLocation(Location):
Expand All @@ -16,6 +19,7 @@ def from_issue(cls, issue) -> Self:


class SonarResult(Result):

@classmethod
def from_issue(cls, issue) -> Self:
rule_id = issue.get("rule", None)
Expand All @@ -25,6 +29,17 @@ def from_issue(cls, issue) -> Self:
locations: list[Location] = [SonarLocation.from_issue(issue)]
return cls(rule_id=rule_id, locations=locations)

def match_location(self, pos, node):
match node:
case cst.Tuple():
new_pos = replace(
pos,
start=replace(pos.start, column=pos.start.column - 1),
end=replace(pos.end, column=pos.end.column + 1),
)
return super().match_location(new_pos, node)
return super().match_location(pos, node)


class SonarResultSet(ResultSet):
@classmethod
Expand Down
2 changes: 2 additions & 0 deletions src/core_codemods/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
from .sonar.sonar_literal_or_new_object_identity import SonarLiteralOrNewObjectIdentity
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

registry = CodemodCollection(
origin="pixee",
Expand Down Expand Up @@ -121,5 +122,6 @@
SonarLiteralOrNewObjectIdentity,
SonarDjangoReceiverOnTop,
SonarExceptionWithoutRaise,
SonarFixAssertTuple,
],
)
38 changes: 24 additions & 14 deletions src/core_codemods/fix_assert_tuple.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,32 @@
import libcst as cst
from typing import List, Union
from core_codemods.api import Metadata, ReviewGuidance, SimpleCodemod
from codemodder.codemods.libcst_transformer import (
LibcstResultTransformer,
LibcstTransformerPipeline,
)
from core_codemods.api import Metadata, ReviewGuidance
from codemodder.change import Change
from codemodder.codemods.utils_mixin import NameResolutionMixin
from core_codemods.api.core_codemod import CoreCodemod


class FixAssertTuple(SimpleCodemod, NameResolutionMixin):
metadata = Metadata(
name="fix-assert-tuple",
summary="Fix `assert` on Non-Empty Tuple Literal",
review_guidance=ReviewGuidance.MERGE_AFTER_CURSORY_REVIEW,
references=[],
)
class FixAssertTupleTransform(LibcstResultTransformer, NameResolutionMixin):
change_description = "Separate assertion on a non-empty tuple literal into multiple assert statements."

def leave_SimpleStatementLine(
self,
original_node: cst.SimpleStatementLine,
updated_node: cst.SimpleStatementLine,
) -> Union[cst.FlattenSentinel, cst.SimpleStatementLine]:
if not self.filter_by_path_includes_or_excludes(
self.node_position(original_node)
):
return updated_node

if len(updated_node.body) == 1 and isinstance(
assert_node := updated_node.body[0], cst.Assert
if len(original_node.body) == 1 and isinstance(
assert_node := original_node.body[0], cst.Assert
):
match assert_test := assert_node.test:
case cst.Tuple():
if not self.node_is_selected(assert_test):
return updated_node

if not assert_test.elements:
return updated_node
new_asserts = self._make_asserts(assert_node)
Expand All @@ -53,3 +51,15 @@ def _report_new_lines(
description=self.change_description,
)
)


FixAssertTuple = CoreCodemod(
metadata=Metadata(
name="fix-assert-tuple",
summary="Fix `assert` on Non-Empty Tuple Literal",
review_guidance=ReviewGuidance.MERGE_AFTER_CURSORY_REVIEW,
references=[],
),
transformer=LibcstTransformerPipeline(FixAssertTupleTransform),
detector=None,
)
12 changes: 12 additions & 0 deletions src/core_codemods/sonar/sonar_fix_assert_tuple.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_assert_tuple import FixAssertTuple

SonarFixAssertTuple = SonarCodemod.from_core_codemod(
name="fix-assert-tuple-S5905",
other=FixAssertTuple,
rules=["python:S5905"],
new_references=[
Reference(url="https://rules.sonarsource.com/python/type/Bug/RSPEC-5905/"),
],
)
8 changes: 2 additions & 6 deletions src/core_codemods/url_sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,10 @@ def __init__(
self.changes_in_file: List[Change] = []

def leave_Call(self, original_node: cst.Call):
pos_to_match = self.node_position(original_node)
if not (
self.filter_by_result(pos_to_match)
and self.filter_by_path_includes_or_excludes(pos_to_match)
):
if not (self.node_is_selected(original_node)):
return

line_number = pos_to_match.start.line
line_number = self.node_position(original_node).start.line
match original_node.args[0].value:
case cst.SimpleString():
return
Expand Down
4 changes: 2 additions & 2 deletions tests/codemods/test_base_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ def __init__(self, context, results, line_exclude=None, line_include=None):
self.line_exclude = line_exclude or []
self.line_include = line_include or []

def filter_by_result(self, pos_to_match):
def filter_by_result(self, node):
return True

def leave_SimpleStatementLine(
self, original_node: cst.SimpleStatementLine, updated_node
):
pos_to_match = self.node_position(original_node)
if self.filter_by_result(
pos_to_match
original_node
) and self.filter_by_path_includes_or_excludes(pos_to_match):
return cst.RemovalSentinel.REMOVE
return original_node
Expand Down
42 changes: 42 additions & 0 deletions tests/codemods/test_sonar_fix_assert_tuple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import json
from core_codemods.sonar.sonar_fix_assert_tuple import SonarFixAssertTuple
from tests.codemods.base_codemod_test import BaseSASTCodemodTest


class TestSonarFixAssertTuple(BaseSASTCodemodTest):
codemod = SonarFixAssertTuple
tool = "sonar"

def test_name(self):
assert self.codemod.name == "fix-assert-tuple-S5905"

def test_simple(self, tmpdir):
input_code = """\
assert (1,2,3)
"""
expected_output = """\
assert 1
assert 2
assert 3
"""
issues = {
"issues": [
{
"rule": "python:S5905",
"component": f"{tmpdir / 'code.py'}",
"textRange": {
"startLine": 1,
"endLine": 1,
"startOffset": 8,
"endOffset": 15,
},
}
]
}
self.run_and_assert(
tmpdir,
input_code,
expected_output,
results=json.dumps(issues),
num_changes=3,
)

0 comments on commit 322399f

Please sign in to comment.