diff --git a/integration_tests/test_sonar_numpy_nan_equality.py b/integration_tests/test_sonar_numpy_nan_equality.py index 344d798c..f5a644d8 100644 --- a/integration_tests/test_sonar_numpy_nan_equality.py +++ b/integration_tests/test_sonar_numpy_nan_equality.py @@ -1,6 +1,6 @@ +from core_codemods.numpy_nan_equality import NumpyNanEqualityTransformer from core_codemods.sonar.sonar_numpy_nan_equality import ( SonarNumpyNanEquality, - SonarNumpyNanEqualityTransformer, ) from integration_tests.base_test import ( BaseIntegrationTest, @@ -34,5 +34,5 @@ class TestNumpyNanEquality(BaseIntegrationTest): # fmt: on expected_line_change = "4" - change_description = SonarNumpyNanEqualityTransformer.change_description + change_description = NumpyNanEqualityTransformer.change_description num_changed_files = 1 diff --git a/src/codemodder/codemods/base_codemod.py b/src/codemodder/codemods/base_codemod.py index 9ec3cfbc..6b7645ce 100644 --- a/src/codemodder/codemods/base_codemod.py +++ b/src/codemodder/codemods/base_codemod.py @@ -139,7 +139,7 @@ def _apply( # It seems like semgrep doesn't like our fully-specified id format self.detector.apply(self.name, context, files_to_analyze) if self.detector - else ResultSet() + else None ) process_file = functools.partial( @@ -180,14 +180,18 @@ def _process_file( self, filename: Path, context: CodemodExecutionContext, - results: ResultSet, + results: ResultSet | None, rules: list[str], ): line_exclude = file_line_patterns(filename, context.path_exclude) line_include = file_line_patterns(filename, context.path_include) - findings_for_rule = [] - for rule in rules: - findings_for_rule.extend(results.results_for_rule_and_file(rule, filename)) + findings_for_rule = None + if results is not None: + findings_for_rule = [] + for rule in rules: + findings_for_rule.extend( + results.results_for_rule_and_file(rule, filename) + ) file_context = FileContext( context.directory, diff --git a/src/codemodder/codemods/base_transformer.py b/src/codemodder/codemods/base_transformer.py index 434d671b..3accac7d 100644 --- a/src/codemodder/codemods/base_transformer.py +++ b/src/codemodder/codemods/base_transformer.py @@ -30,7 +30,7 @@ def apply( self, context: CodemodExecutionContext, file_context: FileContext, - results: list[Result], + results: list[Result] | None, ) -> ChangeSet | None: """ Apply the pipeline to the given file context diff --git a/src/codemodder/codemods/base_visitor.py b/src/codemodder/codemods/base_visitor.py index e50c9178..8ea44371 100644 --- a/src/codemodder/codemods/base_visitor.py +++ b/src/codemodder/codemods/base_visitor.py @@ -7,12 +7,14 @@ # TODO: this should just be part of BaseTransformer and BaseVisitor? class UtilsMixin: - results: list[Result] + results: list[Result] | None - def __init__(self, results: list[Result]): + def __init__(self, results: list[Result] | None): self.results = results def filter_by_result(self, pos_to_match): + if self.results is None: + return True return any( location.match(pos_to_match) for result in self.results @@ -31,9 +33,6 @@ def filter_by_path_includes_or_excludes(self, pos_to_match): return True def node_is_selected(self, node) -> bool: - if not self.results: - return False - pos_to_match = self.node_position(node) return self.filter_by_result( pos_to_match @@ -50,7 +49,7 @@ def lineno_for_node(self, node): class BaseTransformer(VisitorBasedCodemodCommand, UtilsMixin): METADATA_DEPENDENCIES: Tuple[Any, ...] = (PositionProvider,) - def __init__(self, context, results: list[Result]): + def __init__(self, context, results: list[Result] | None): super().__init__(context) UtilsMixin.__init__(self, results) diff --git a/src/codemodder/codemods/libcst_transformer.py b/src/codemodder/codemods/libcst_transformer.py index f7d459ea..50c57fa5 100644 --- a/src/codemodder/codemods/libcst_transformer.py +++ b/src/codemodder/codemods/libcst_transformer.py @@ -45,7 +45,7 @@ class LibcstResultTransformer( def __init__( self, context: CodemodContext, - results: list[Result], + results: list[Result] | None, file_context: FileContext, _transformer: bool = False, ): @@ -56,7 +56,7 @@ def __init__( @classmethod def transform( - cls, module: cst.Module, results: list[Result], file_context: FileContext + cls, module: cst.Module, results: list[Result] | None, file_context: FileContext ) -> cst.Module: wrapper = cst.MetadataWrapper(module) codemod = cls( @@ -260,7 +260,7 @@ def apply( self, context: CodemodExecutionContext, file_context: FileContext, - results: list[Result], + results: list[Result] | None, ) -> ChangeSet | None: file_path = file_context.file_path diff --git a/src/codemodder/codemods/sonar.py b/src/codemodder/codemods/sonar.py index 482cfd4a..8870ec4a 100644 --- a/src/codemodder/codemods/sonar.py +++ b/src/codemodder/codemods/sonar.py @@ -1,9 +1,11 @@ from pathlib import Path +from codemodder.codemods.base_codemod import Metadata, Reference from codemodder.codemods.base_detector import BaseDetector +from codemodder.codemods.base_transformer import BaseTransformerPipeline from codemodder.context import CodemodExecutionContext from codemodder.result import ResultSet from codemodder.sonar_results import SonarResultSet -from core_codemods.api.core_codemod import SASTCodemod +from core_codemods.api.core_codemod import CoreCodemod, SASTCodemod class SonarCodemod(SASTCodemod): @@ -11,6 +13,33 @@ class SonarCodemod(SASTCodemod): def origin(self): return "sonar" + @classmethod + def from_core_codemod( + cls, + name: str, + other: CoreCodemod, + rules: list[str], + transformer: BaseTransformerPipeline | None = None, + new_references: list[Reference] | None = None, + ): # pylint: disable=too-many-arguments + return SonarCodemod( + metadata=Metadata( + name=name, + summary="Sonar: " + other.summary, + review_guidance=other._metadata.review_guidance, # pylint: disable=protected-access + references=( + other.references + if not new_references + else other.references + new_references + ), + description=f"This codemod acts upon the following Sonar rules: {str(rules)[1:-1]}.\n\n" + + other.description, + ), + transformer=transformer if transformer else other.transformer, + detector=SonarDetector(), + requested_rules=rules, + ) + class SonarDetector(BaseDetector): _lazy_cache = None diff --git a/src/codemodder/file_context.py b/src/codemodder/file_context.py index 17fc9ff7..06aa5156 100644 --- a/src/codemodder/file_context.py +++ b/src/codemodder/file_context.py @@ -17,7 +17,7 @@ class FileContext: # pylint: disable=too-many-instance-attributes file_path: Path line_exclude: list[int] = field(default_factory=list) line_include: list[int] = field(default_factory=list) - findings: list[Result] = field(default_factory=list) + findings: list[Result] | None = field(default_factory=list) dependencies: set[Dependency] = field(default_factory=set) codemod_changes: list[Change] = field(default_factory=list) results: list[ChangeSet] = field(default_factory=list) diff --git a/src/core_codemods/numpy_nan_equality.py b/src/core_codemods/numpy_nan_equality.py index 4d6e988d..62b8ea76 100644 --- a/src/core_codemods/numpy_nan_equality.py +++ b/src/core_codemods/numpy_nan_equality.py @@ -50,7 +50,7 @@ def _is_np_nan_eq(self, left: cst.BaseExpression, target: cst.ComparisonTarget): def leave_Comparison( self, original_node: cst.Comparison, updated_node: cst.Comparison ) -> cst.BaseExpression: - if self.filter_by_path_includes_or_excludes(self.node_position(original_node)): + if self.node_is_selected(original_node): match original_node: case cst.Comparison(comparisons=[cst.ComparisonTarget() as target]): maybe_nan_eq = self._is_np_nan_eq(original_node.left, target) diff --git a/src/core_codemods/sonar/sonar_numpy_nan_equality.py b/src/core_codemods/sonar/sonar_numpy_nan_equality.py index a8b6f716..0fb448b0 100644 --- a/src/core_codemods/sonar/sonar_numpy_nan_equality.py +++ b/src/core_codemods/sonar/sonar_numpy_nan_equality.py @@ -1,42 +1,15 @@ -import libcst as cst from codemodder.codemods.base_codemod import Reference -from codemodder.codemods.libcst_transformer import ( - LibcstTransformerPipeline, -) -from codemodder.codemods.sonar import SonarDetector from codemodder.codemods.sonar import SonarCodemod -from core_codemods.api import Metadata from core_codemods.numpy_nan_equality import ( NumpyNanEquality, - NumpyNanEqualityTransformer, ) - -class SonarNumpyNanEqualityTransformer(NumpyNanEqualityTransformer): - def leave_Comparison( - self, original_node: cst.Comparison, updated_node: cst.Comparison - ) -> cst.BaseExpression: - if self.filter_by_result(self.node_position(original_node)): - return super().leave_Comparison(original_node, updated_node) - return updated_node - - -rules = ["python:S6725"] - -SonarNumpyNanEquality = SonarCodemod( - metadata=Metadata( - name="numpy-nan-equality-S6725", - summary="Sonar: " + NumpyNanEquality.summary, - review_guidance=NumpyNanEquality._metadata.review_guidance, # pylint: disable=protected-access - references=NumpyNanEquality.references - + [ - Reference(url="https://rules.sonarsource.com/python/type/Bug/RSPEC-6725/"), - ], - description=f"This codemod acts upon the following Sonar rules: {str(rules)[1:-1]}.\n\n" - + NumpyNanEquality.description, - ), - transformer=LibcstTransformerPipeline(SonarNumpyNanEqualityTransformer), - detector=SonarDetector(), - requested_rules=rules, +SonarNumpyNanEquality = SonarCodemod.from_core_codemod( + name="numpy-nan-equality-S6725", + other=NumpyNanEquality, + rules=["python:S6725"], + new_references=[ + Reference(url="https://rules.sonarsource.com/python/type/Bug/RSPEC-6725/"), + ], )