From 5d00b54b1c089b0d3dc782da8eb6f3a0971005a2 Mon Sep 17 00:00:00 2001 From: jackdewinter Date: Sun, 31 Dec 2023 14:11:00 -0800 Subject: [PATCH] https://github.com/jackdewinter/pymarkdown/issues/940 (#942) * https://github.com/jackdewinter/pymarkdown/issues/940 --- changelog.md | 2 + docs/developer.md | 4 +- docs/rules/rule_md006.md | 10 ++ publish/coverage.json | 8 +- publish/test-results.json | 2 +- pymarkdown/plugins/rule_md_006.py | 50 +++++- test/rules/test_md006.py | 289 +++++++++++++++++++++++++++++- 7 files changed, 349 insertions(+), 16 deletions(-) diff --git a/changelog.md b/changelog.md index 259f02cc2..4f55dd659 100644 --- a/changelog.md +++ b/changelog.md @@ -39,6 +39,8 @@ for a version 1.0 release in early 2024. - Rule MD005 - Added fix options - [Issue 938](https://github.com/jackdewinter/pymarkdown/issues/938) - Rule MD027 - Added fix options +- [Issue 940](https://github.com/jackdewinter/pymarkdown/issues/940) + - Rule MD006 (disabled) - Added fix options ### Changed diff --git a/docs/developer.md b/docs/developer.md index 7a410def8..d97a120d2 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -141,12 +141,15 @@ completed_file possible -> MD005, Md007 possible -> MD019/MD021, MD023 possible -> md027 19/21/23/05/07 when blanks inside of list + -> mod027 and most others Md001 Atx/SetExt hash_count MD004 UnorderedListStartMarkdownToken list_start_sequence Md005 NewListItemMarkdownToken indent_level, extracted_whitespace UnorderedListStartMarkdownToken indent_level, extracted_whitespace, column_number, leading_spaces OrderedListStartMarkdownToken indent_level, extracted_whitespace, column_number, leading_spaces +MD006(d) NewListItemMarkdownToken indent_level, extracted_whitespace + UnorderedListStartMarkdownToken indent_level, extracted_whitespace MD007 NewListItemMarkdownToken indent_level, extracted_whitespace UnorderedListStartMarkdownToken indent_level, extracted_whitespace MD009 line -> no trailing spaces @@ -175,5 +178,4 @@ MD027 BlankLineMarkdownToken extracted_whitespace UnorderedListStartMarkdownToken indent_level, extracted_whitespace, column_number, leading_spaces OrderedListStartMarkdownToken indent_level, extracted_whitespace, column_number, leading_spaces -md006 md030 diff --git a/docs/rules/rule_md006.md b/docs/rules/rule_md006.md index 0de9d2eff..ca169f82e 100644 --- a/docs/rules/rule_md006.md +++ b/docs/rules/rule_md006.md @@ -5,6 +5,10 @@ | `md006` | | `ul-start-left` | +| Autofix Available | +| --- | +| Yes | + ## Summary Consider starting bulleted lists at the beginning of the line. @@ -91,3 +95,9 @@ This rule is largely inspired by the MarkdownLint rule It is not clear how this rule, which is disabled by default, differs from Rule Md007. To make sure this rule is well-rounded, it has been changed to work with nested list blocks and block quotes. + +## Fix Description + +The containers will be altered so that they start at the beginning of "the line". +As that definition was not clearly understood [Rule MD007](./rule_md007.md) was +created to more clearly handle the issue. diff --git a/publish/coverage.json b/publish/coverage.json index 8456b12c7..e1f9d200e 100644 --- a/publish/coverage.json +++ b/publish/coverage.json @@ -2,12 +2,12 @@ "projectName": "pymarkdown", "reportSource": "pytest", "branchLevel": { - "totalMeasured": 4675, - "totalCovered": 4675 + "totalMeasured": 4679, + "totalCovered": 4679 }, "lineLevel": { - "totalMeasured": 19102, - "totalCovered": 19102 + "totalMeasured": 19111, + "totalCovered": 19111 } } diff --git a/publish/test-results.json b/publish/test-results.json index 74c6c9e78..bbce2bf7a 100644 --- a/publish/test-results.json +++ b/publish/test-results.json @@ -1148,7 +1148,7 @@ }, { "name": "test.rules.test_md006", - "totalTests": 16, + "totalTests": 22, "failedTests": 0, "errorTests": 0, "skippedTests": 0, diff --git a/pymarkdown/plugins/rule_md_006.py b/pymarkdown/plugins/rule_md_006.py index c633a33c2..c6d32985d 100644 --- a/pymarkdown/plugins/rule_md_006.py +++ b/pymarkdown/plugins/rule_md_006.py @@ -5,7 +5,7 @@ from typing import List, cast from pymarkdown.general.parser_helper import ParserHelper -from pymarkdown.plugin_manager.plugin_details import PluginDetails +from pymarkdown.plugin_manager.plugin_details import PluginDetailsV2 from pymarkdown.plugin_manager.plugin_scan_context import PluginScanContext from pymarkdown.plugin_manager.rule_plugin import RulePlugin from pymarkdown.tokens.block_quote_markdown_token import BlockQuoteMarkdownToken @@ -23,18 +23,18 @@ def __init__(self) -> None: super().__init__() self.__token_stack: List[MarkdownToken] = [] - def get_details(self) -> PluginDetails: + def get_details(self) -> PluginDetailsV2: """ Get the details for the plugin. """ - return PluginDetails( + return PluginDetailsV2( plugin_name="ul-start-left", plugin_id="MD006", plugin_enabled_by_default=False, plugin_description="Consider starting bulleted lists at the beginning of the line", plugin_version="0.5.0", - plugin_interface_version=1, plugin_url="https://github.com/jackdewinter/pymarkdown/blob/main/docs/rules/rule_md006.md", + plugin_supports_fix=True, ) def starting_new_file(self) -> None: @@ -70,12 +70,46 @@ def next_token(self, context: PluginScanContext, token: MarkdownToken) -> None: self.__token_stack.append(token) if token.is_unordered_list_start: expected_indent = self.__calculate_expected_indent() - if token.column_number != (1 + expected_indent): - self.report_next_token_error(context, token) + if delta := (1 + expected_indent) - token.column_number: + self.__report_or_fix(context, token, delta) elif token.is_list_end or token.is_block_quote_end: del self.__token_stack[-1] elif token.is_new_list_item: if self.__token_stack[-1].is_unordered_list_start: expected_indent = self.__calculate_expected_indent() - if token.column_number != (1 + expected_indent): - self.report_next_token_error(context, token) + if delta := (1 + expected_indent) - token.column_number: + self.__report_or_fix(context, token, delta) + + def __report_or_fix( + self, context: PluginScanContext, token: MarkdownToken, adjust_amount: int + ) -> None: + if context.in_fix_mode: + list_start_token = cast(ListStartMarkdownToken, token) + self.register_fix_token_request( + context, + token, + "next_token", + "indent_level", + list_start_token.indent_level + adjust_amount, + ) + if not token.is_new_list_item: + self.register_fix_token_request( + context, token, "next_token", "extracted_whitespace", "" + ) + self.register_fix_token_request( + context, + token, + "next_token", + "column_number", + list_start_token.column_number + adjust_amount, + ) + else: + self.register_fix_token_request( + context, + token, + "next_token", + "extracted_whitespace", + list_start_token.extracted_whitespace[:adjust_amount], + ) + else: + self.report_next_token_error(context, token) diff --git a/test/rules/test_md006.py b/test/rules/test_md006.py index d16e31801..591e79b81 100644 --- a/test/rules/test_md006.py +++ b/test/rules/test_md006.py @@ -3,9 +3,12 @@ """ import os from test.markdown_scanner import MarkdownScanner +from test.utils import assert_file_is_as_expected, copy_to_temp_file import pytest +source_path = os.path.join("test", "resources", "rules", "md006") + os.sep + @pytest.mark.rules def test_md006_good_indentation(): @@ -111,7 +114,50 @@ def test_md006_bad_indentation_x(): @pytest.mark.rules -def test_md006_bad_indentation_unordered(): +def test_md006_bad_indentation_x_fix(): + """ + Test to make sure this rule does trigger with a document that + is only level 1 lists with a single space of indentation. + """ + + # Arrange + scanner = MarkdownScanner() + with copy_to_temp_file(source_path + "bad_indentation.md") as temp_source_path: + original_file_contents = """ * First Item + * Second Item +""" + assert_file_is_as_expected(temp_source_path, original_file_contents) + + supplied_arguments = [ + "--enable-rules", + "MD006", + "--disable-rules", + "MD007", + "-x-fix", + "scan", + temp_source_path, + ] + + expected_return_code = 3 + expected_output = f"Fixed: {temp_source_path}" + expected_error = "" + + expected_file_contents = """* First Item +* Second Item +""" + + # Act + execute_results = scanner.invoke_main(arguments=supplied_arguments) + + # Assert + execute_results.assert_results( + expected_output, expected_error, expected_return_code + ) + assert_file_is_as_expected(temp_source_path, expected_file_contents) + + +@pytest.mark.rules +def test_md006_good_indentation_unordered(): """ Test to make sure this rule does trigger with a document that is only level 1 lists with a single space of indentation. @@ -186,7 +232,52 @@ def test_md006_bad_indentation_in_block_quote(): @pytest.mark.rules -def test_md006_good_ignore_bad_second_level(): +def test_md006_bad_indentation_in_block_quote_fix(): + """ + Test to make sure this rule does trigger with a document that + is only level 1 lists with a single space of indentation. + """ + + # Arrange + scanner = MarkdownScanner() + with copy_to_temp_file( + source_path + "bad_indentation_in_block_quote.md" + ) as temp_source_path: + original_file_contents = """> * First Item +> * Second Item +""" + assert_file_is_as_expected(temp_source_path, original_file_contents) + + supplied_arguments = [ + "--enable-rules", + "MD006", + "--disable-rules", + "MD007,md027", + "-x-fix", + "scan", + temp_source_path, + ] + + expected_return_code = 3 + expected_output = f"Fixed: {temp_source_path}" + expected_error = "" + + expected_file_contents = """> * First Item +> * Second Item +""" + + # Act + execute_results = scanner.invoke_main(arguments=supplied_arguments) + + # Assert + execute_results.assert_results( + expected_output, expected_error, expected_return_code + ) + assert_file_is_as_expected(temp_source_path, expected_file_contents) + + +@pytest.mark.rules +def test_md006_bad_ignore_bad_second_level(): """ Test to make sure this rule does not trigger with a document that is nested lists with level 1 lists properly indented. @@ -224,6 +315,57 @@ def test_md006_good_ignore_bad_second_level(): ) +@pytest.mark.rules +def test_md006_bad_ignore_bad_second_level_fix(): + """ + Test to make sure this rule does trigger with a document that + is only level 1 lists with a single space of indentation. + """ + + # Arrange + scanner = MarkdownScanner() + with copy_to_temp_file( + source_path + "good_ignore_bad_second_level.md" + ) as temp_source_path: + original_file_contents = """* First Item + * First-First + * First-Second + * First-Third +* Second Item +""" + assert_file_is_as_expected(temp_source_path, original_file_contents) + + supplied_arguments = [ + "--enable-rules", + "MD006", + "--disable-rules", + "MD005,Md007", + "-x-fix", + "scan", + temp_source_path, + ] + + expected_return_code = 3 + expected_output = f"Fixed: {temp_source_path}" + expected_error = "" + + expected_file_contents = """* First Item + * First-First + * First-Second + * First-Third +* Second Item +""" + + # Act + execute_results = scanner.invoke_main(arguments=supplied_arguments) + + # Assert + execute_results.assert_results( + expected_output, expected_error, expected_return_code + ) + assert_file_is_as_expected(temp_source_path, expected_file_contents) + + @pytest.mark.rules def test_md006_good_not_ordered(): """ @@ -430,6 +572,51 @@ def test_md006_bad_indentation_ordered_in_unordered(): ) +@pytest.mark.rules +def test_md006_bad_indentation_ordered_in_unordered_fix(): + """ + Test to make sure this rule does trigger with a document that + is only level 1 lists with a single space of indentation. + """ + + # Arrange + scanner = MarkdownScanner() + with copy_to_temp_file( + source_path + "bad_indentation_ordered_in_unordered.md" + ) as temp_source_path: + original_file_contents = """ * First Item + 1. Second Item +""" + assert_file_is_as_expected(temp_source_path, original_file_contents) + + supplied_arguments = [ + "--enable-rules", + "MD006", + "--disable-rules", + "MD007", + "-x-fix", + "scan", + temp_source_path, + ] + + expected_return_code = 3 + expected_output = f"Fixed: {temp_source_path}" + expected_error = "" + + expected_file_contents = """* First Item + 1. Second Item +""" + + # Act + execute_results = scanner.invoke_main(arguments=supplied_arguments) + + # Assert + execute_results.assert_results( + expected_output, expected_error, expected_return_code + ) + assert_file_is_as_expected(temp_source_path, expected_file_contents) + + @pytest.mark.rules def test_md006_bad_indentation_unordered_in_ordered(): """ @@ -466,6 +653,51 @@ def test_md006_bad_indentation_unordered_in_ordered(): ) +@pytest.mark.rules +def test_md006_bad_indentation_unordered_in_ordered_fix(): + """ + Test to make sure this rule does trigger with a document that + is only level 1 lists with a single space of indentation. + """ + + # Arrange + scanner = MarkdownScanner() + with copy_to_temp_file( + source_path + "bad_indentation_unordered_in_ordered.md" + ) as temp_source_path: + original_file_contents = """ 1. First Item + - Second Item +""" + assert_file_is_as_expected(temp_source_path, original_file_contents) + + supplied_arguments = [ + "--enable-rules", + "MD006", + "--disable-rules", + "MD007", + "-x-fix", + "scan", + temp_source_path, + ] + + expected_return_code = 3 + expected_output = f"Fixed: {temp_source_path}" + expected_error = "" + + expected_file_contents = """ 1. First Item + - Second Item +""" + + # Act + execute_results = scanner.invoke_main(arguments=supplied_arguments) + + # Assert + execute_results.assert_results( + expected_output, expected_error, expected_return_code + ) + assert_file_is_as_expected(temp_source_path, expected_file_contents) + + @pytest.mark.rules def test_md006_good_indentation_nested(): """ @@ -539,6 +771,59 @@ def test_md006_bad_indentation_nested(): ) +@pytest.mark.rules +def test_md006_bad_indentation_nested_fix(): + """ + Test to make sure this rule does trigger with a document that + is only level 1 lists with a single space of indentation. + """ + + # Arrange + scanner = MarkdownScanner() + with copy_to_temp_file( + source_path + "bad_indentation_nested.md" + ) as temp_source_path: + original_file_contents = """- top level 1 + - First Item + - Second Item +- top level 2 + - First Item + - Second Item +""" + assert_file_is_as_expected(temp_source_path, original_file_contents) + + supplied_arguments = [ + "--enable-rules", + "MD006", + "--disable-rules", + "MD007", + "-x-fix", + "scan", + temp_source_path, + ] + + expected_return_code = 3 + expected_output = f"Fixed: {temp_source_path}" + expected_error = "" + + expected_file_contents = """- top level 1 + - First Item + - Second Item +- top level 2 + - First Item + - Second Item +""" + + # Act + execute_results = scanner.invoke_main(arguments=supplied_arguments) + + # Assert + execute_results.assert_results( + expected_output, expected_error, expected_return_code + ) + assert_file_is_as_expected(temp_source_path, expected_file_contents) + + @pytest.mark.rules def test_md006_issue_478(): """