diff --git a/tests/plugins/test_spaces_before_dots.py b/tests/plugins/test_spaces_before_dots.py index e1d54647..38ff4849 100644 --- a/tests/plugins/test_spaces_before_dots.py +++ b/tests/plugins/test_spaces_before_dots.py @@ -4,6 +4,8 @@ from pathlib import Path from tests.plugins import PluginTestCase +from troubadix.helper import CURRENT_ENCODING +from troubadix.plugin import LinterFix from troubadix.plugins.spaces_before_dots import CheckSpacesBeforeDots @@ -30,7 +32,7 @@ def test_fail(self): script_tag(name:"summary", value:"Foo Bar ."); script_tag(name:"vuldetect", value:"Foo Bar ."); script_tag(name:"insight", value:"Foo Bar ."); - script_tag(name:"impact", value:"Foo . Bar"); + script_tag(name:"impact", value:"Foo . Bar . Foo"); """ 'script_tag(name:"affected", value:"Foo\n.\nBar.");' 'script_tag(name:"solution", value:"Foo Bar\n.");' @@ -40,7 +42,7 @@ def test_fail(self): ) plugin = CheckSpacesBeforeDots(fake_context) results = list(plugin.run()) - self.assertEqual(len(results), 6) + self.assertEqual(len(results), 7) self.assertEqual( results[0].message, "value of script_tag summary has alteast one occurence of excess" @@ -58,3 +60,44 @@ def test_ignore(self): results = list(plugin.run()) self.assertEqual(len(results), 0) + + def test_fix(self): + with self.create_directory() as tempdir: + path = tempdir / "file.nasl" + content = ( + """ + script_tag(name:"summary", value:"Foo Bar ."); + script_tag(name:"vuldetect", value:"Foo Bar ."); + script_tag(name:"insight", value:"Foo Bar ."); + script_tag(name:"impact", value:"Foo . Bar . Foo"); + """ + 'script_tag(name:"affected", value:"Foo\n.\nBar.");' + 'script_tag(name:"solution", value:"Foo Bar\n.");' + ) + expected_modified_content = ( + """ + script_tag(name:"summary", value:"Foo Bar."); + script_tag(name:"vuldetect", value:"Foo Bar."); + script_tag(name:"insight", value:"Foo Bar."); + script_tag(name:"impact", value:"Foo. Bar. Foo"); + """ + 'script_tag(name:"affected", value:"Foo.\nBar.");' + 'script_tag(name:"solution", value:"Foo Bar.");' + ) + path.write_text(content, encoding=CURRENT_ENCODING) + + fake_context = self.create_file_plugin_context( + nasl_file=path, file_content=content + ) + + plugin = CheckSpacesBeforeDots(fake_context) + + # keep list() to consume the iterator + list(plugin.run()) + + results = list(plugin.fix()) + self.assertEqual(len(results), 1) + + self.assertIsInstance(results[0], LinterFix) + modified_content = path.read_text(encoding=CURRENT_ENCODING) + self.assertEqual(modified_content, expected_modified_content) diff --git a/troubadix/plugins/spaces_before_dots.py b/troubadix/plugins/spaces_before_dots.py index 194f489b..f23e9719 100644 --- a/troubadix/plugins/spaces_before_dots.py +++ b/troubadix/plugins/spaces_before_dots.py @@ -2,13 +2,20 @@ # SPDX-FileCopyrightText: 2024 Greenbone AG import re from collections.abc import Iterator +from operator import itemgetter from pathlib import Path +from troubadix.helper import CURRENT_ENCODING from troubadix.helper.patterns import ( ScriptTag, get_script_tag_pattern, ) -from troubadix.plugin import FileContentPlugin, LinterResult, LinterWarning +from troubadix.plugin import ( + FileContentPlugin, + LinterFix, + LinterResult, + LinterWarning, +) TAGS = [ ScriptTag.SUMMARY, @@ -22,7 +29,7 @@ # Regex pattern to match: # 1. A dot preceded and/or followed by any whitespace character (floating between words) # 2. A dot preceded by any whitespace character at the end of the string -PATTERN = re.compile(r"\s\.(\s|$)") +PATTERN = re.compile(r"\s+\.(\s|$)") class CheckSpacesBeforeDots(FileContentPlugin): @@ -35,6 +42,7 @@ def check_content( This plugin checks for excess whitespace before a dot in script_tags that have full sentence values """ + self.matches = [] if nasl_file.suffix == ".inc": return for tag in TAGS: @@ -42,7 +50,11 @@ def check_content( match = pattern.search(file_content) if match: value = match.group("value") - if PATTERN.search(value): + value_start = match.start("value") + + for excess_match in PATTERN.finditer(value): + whitespace_pos = excess_match.start() + value_start + self.matches.append((whitespace_pos, excess_match.group())) fullmatch = match.group() yield LinterWarning( f"value of script_tag {match.group('name')} has alteast" @@ -51,3 +63,30 @@ def check_content( file=nasl_file, plugin=self.name, ) + + def fix(self) -> Iterator[LinterResult]: + + if not self.matches: + return + + # Sort matches by position, descending order to avoid messing up indices during replacement + self.matches.sort(reverse=True, key=itemgetter(0)) + + file_content = self.context.file_content + for pos, match_str in self.matches: + # Replace the match by removing the excess whitespace before the dot + fixed_str = re.sub(r"\s+\.", ".", match_str) + file_content = ( + file_content[:pos] + + fixed_str + + file_content[pos + len(match_str) :] + ) + + with open(self.context.nasl_file, "w", encoding=CURRENT_ENCODING) as f: + f.write(file_content) + + yield LinterFix( + "Excess spaces were removed", + file=self.context.nasl_file, + plugin=self.name, + )