diff --git a/README.md b/README.md index 386cb683..c92a1cc1 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,7 @@ following rules are enabled by default: * `cd_parent` – changes `cd..` to `cd ..`; * `chmod_x` – add execution bit; * `composer_not_command` – fixes composer command name; +* `composer_not_package` – fixes composer misspelled package names; * `cp_omitting_directory` – adds `-a` when you `cp` directory; * `cpp11` – adds missing `-std=c++11` to `g++` or `clang++`; * `dirty_untar` – fixes `tar x` command that untarred in the current directory; diff --git a/tests/rules/test_composer_not_command.py b/tests/rules/test_composer_not_command.py index 33823cfc..518b97a5 100644 --- a/tests/rules/test_composer_not_command.py +++ b/tests/rules/test_composer_not_command.py @@ -2,55 +2,48 @@ import pytest from thefuck.rules.composer_not_command import match, get_new_command from thefuck.types import Command +# tested with composer version 1.9.0 -@pytest.fixture -def composer_not_command(): - # that weird spacing is part of the actual command output - return ( - '\n' - '\n' - ' \n' - ' [InvalidArgumentException] \n' - ' Command "udpate" is not defined. \n' - ' Did you mean this? \n' - ' update \n' - ' \n' - '\n' - '\n' - ) +# command: composer udpate +case_single_command_with_one_suggestion = ("composer udpate", r'\n' + ' ''\n' + r' [Symfony\Component\Console\Exception\CommandNotFoundException] ''\n' + ' Command "udpate" is not defined. ''\n' + ' ''\n' + ' Did you mean this? ''\n' + ' update ''\n' + ' ''\n' + '\n' + ) +case_single_command_with_one_suggestion_expected = "composer update" + +# command: composer selupdate +case_single_command_with_many_suggestions = ("composer selupdate", r'\n' + ' ''\n' + r' [Symfony\Component\Console\Exception\CommandNotFoundException] ''\n' + ' Command "selupdate" is not defined. ''\n' + ' ''\n' + ' Did you mean one of these? ''\n' + ' update ''\n' + ' self-update ''\n' + ' selfupdate ''\n' + ' ''\n' + '\n' + ) +case_single_command_with_many_suggesitons_expected = ["composer update", "composer self-update", "composer selfupdate"] -@pytest.fixture -def composer_not_command_one_of_this(): - # that weird spacing is part of the actual command output - return ( - '\n' - '\n' - ' \n' - ' [InvalidArgumentException] \n' - ' Command "pdate" is not defined. \n' - ' Did you mean one of these? \n' - ' selfupdate \n' - ' self-update \n' - ' update \n' - ' \n' - '\n' - '\n' - ) +@pytest.mark.parametrize('command', [Command(*v) for v in [ + case_single_command_with_one_suggestion, + case_single_command_with_many_suggestions +]]) +def test_match(command): + assert match(command) -def test_match(composer_not_command, composer_not_command_one_of_this): - assert match(Command('composer udpate', - composer_not_command)) - assert match(Command('composer pdate', - composer_not_command_one_of_this)) - assert not match(Command('ls update', composer_not_command)) - - -def test_get_new_command(composer_not_command, composer_not_command_one_of_this): - assert (get_new_command(Command('composer udpate', - composer_not_command)) - == 'composer update') - assert (get_new_command(Command('composer pdate', - composer_not_command_one_of_this)) - == 'composer selfupdate') +@pytest.mark.parametrize('command, new_command', [(Command(*t[0]), t[1]) for t in [ + (case_single_command_with_one_suggestion, case_single_command_with_one_suggestion_expected), + (case_single_command_with_many_suggestions, case_single_command_with_many_suggesitons_expected) +]]) +def test_get_new_command(command, new_command): + assert get_new_command(command) == new_command diff --git a/tests/rules/test_composer_not_package.py b/tests/rules/test_composer_not_package.py new file mode 100644 index 00000000..51a32c44 --- /dev/null +++ b/tests/rules/test_composer_not_package.py @@ -0,0 +1,118 @@ +import pytest +from thefuck.rules.composer_not_package import match, get_new_command +from thefuck.types import Command + +# tested with composer version 1.9.0 + +# command: +# composer require laravel-nova-csv-import +# the one that started it all +case_original = ("composer require laravel-nova-csv-import", '\n' + ' ''\n' + ' [InvalidArgumentException] ''\n' + ' Could not find package laravel-nova-csv-import. ''\n' + ' ''\n' + ' Did you mean this? ''\n' + ' simonhamp/laravel-nova-csv-import ''\n' + ' ''\n' + 'require [--dev] [--prefer-source] [--prefer-dist] [--no-progress] [--no-suggest] [--no-update] [--no-scripts] [--update-no-dev] [--update-with-dependencies] [--update-with-all-dependencies] [--ignore-platform-reqs] [--prefer-stable] [--prefer-lowest] [--sort-packages] [-o|--optimize-autoloader] [-a|--classmap-authoritative] [--apcu-autoloader] [--] []...''\n' + '\n') +case_original_expected = "composer require simonhamp/laravel-nova-csv-import" + +# command: +# composer require datrack/elasticroute +case_single_suggestion = ("composer require datrack/elasticroute", '\n' + ' ''\n' + ' [InvalidArgumentException] ''\n' + ' Could not find package datrack/elasticroute. ''\n' + ' ''\n' + ' Did you mean this? ''\n' + ' detrack/elasticroute ''\n' + ' ''\n' + 'require [--dev] [--prefer-source] [--prefer-dist] [--no-progress] [--no-suggest] [--no-update] [--no-scripts] [--update-no-dev] [--update-with-dependencies] [--update-with-all-dependencies] [--ignore-platform-reqs] [--prefer-stable] [--prefer-lowest] [--sort-packages] [-o|--optimize-autoloader] [-a|--classmap-authoritative] [--apcu-autoloader] [--] []...''\n' + '\n') +case_single_suggestion_expected = "composer require detrack/elasticroute" + +# command: +# composer require potato +case_many_suggestions = ("composer require potato", '\n' + ' ''\n' + ' [InvalidArgumentException] ''\n' + ' Could not find package potato. ''\n' + ' ''\n' + ' Did you mean one of these? ''\n' + ' drteam/potato ''\n' + ' florence/potato ''\n' + ' kola/potato-orm ''\n' + ' jsyqw/potato-bot ''\n' + ' vundi/potato-orm ''\n' + ' ''\n\n' + 'require [--dev] [--prefer-source] [--prefer-dist] [--no-progress] [--no-suggest] [--no-update] [--no-scripts] [--update-no-dev] [--update-with-dependencies] [--update-with-all-dependencies] [--ignore-platform-reqs] [--prefer-stable] [--prefer-lowest] [--sort-packages] [-o|--optimize-autoloader] [-a|--classmap-authoritative] [--apcu-autoloader] [--] []...''\n' + '\n') +case_many_suggestions_expected = ["composer require drteam/potato", + "composer require florence/potato", + "composer require kola/potato-orm", + "composer require jsyqw/potato-bot", + "composer require vundi/potato-orm"] + +# command: +# composer require datrack/elasticroute:* +case_single_package_with_version_constraint = ("composer require datrack/elasticroute:*", '\n' + ' ''\n' + ' [InvalidArgumentException] ''\n' + ' Could not find package datrack/elasticroute. ''\n' + ' ''\n' + ' Did you mean this? ''\n' + ' detrack/elasticroute ''\n' + ' ''\n\n' + 'require [--dev] [--prefer-source] [--prefer-dist] [--no-progress] [--no-suggest] [--no-update] [--no-scripts] [--update-no-dev] [--update-with-dependencies] [--update-with-all-dependencies] [--ignore-platform-reqs] [--prefer-stable] [--prefer-lowest] [--sort-packages] [-o|--optimize-autoloader] [-a|--classmap-authoritative] [--apcu-autoloader] [--] []...''\n' + '\n') +case_single_package_with_version_constraint_expected = "composer require detrack/elasticroute:*" + + +# command: +# composer require potato:* +case_single_package_with_version_constraint_many_suggestions = ("composer require potato:1.2.3", '\n' + ' ''\n' + ' [InvalidArgumentException] ''\n' + ' Could not find package potato. ''\n' + ' ''\n' + ' Did you mean one of these? ''\n' + ' drteam/potato ''\n' + ' florence/potato ''\n' + ' kola/potato-orm ''\n' + ' jsyqw/potato-bot ''\n' + ' vundi/potato-orm ''\n' + ' ''\n\n' + 'require [--dev] [--prefer-source] [--prefer-dist] [--no-progress] [--no-suggest] [--no-update] [--no-scripts] [--update-no-dev] [--update-with-dependencies] [--update-with-all-dependencies] [--ignore-platform-reqs] [--prefer-stable] [--prefer-lowest] [--sort-packages] [-o|--optimize-autoloader] [-a|--classmap-authoritative] [--apcu-autoloader] [--] []...''\n' + + '\n' + ) +case_single_package_with_version_constraint_many_suggestions_expected = ["composer require drteam/potato:1.2.3", + "composer require florence/potato:1.2.3", + "composer require kola/potato-orm:1.2.3", + "composer require jsyqw/potato-bot:1.2.3", + "composer require vundi/potato-orm:1.2.3"] + + +@pytest.mark.parametrize('command', [Command(*v) for v in [ + case_original, + case_single_suggestion, + case_many_suggestions, + case_single_package_with_version_constraint, + case_single_package_with_version_constraint_many_suggestions +]]) +def test_match(command): + assert match(command) + + +@pytest.mark.parametrize('command, new_command', [(Command(*t[0]), t[1]) for t in [ + (case_original, case_original_expected), + (case_single_suggestion, case_single_suggestion_expected), + (case_many_suggestions, case_many_suggestions_expected), + (case_single_package_with_version_constraint, case_single_package_with_version_constraint_expected), + (case_single_package_with_version_constraint_many_suggestions, + case_single_package_with_version_constraint_many_suggestions_expected), +]]) +def test_get_new_command(command, new_command): + assert get_new_command(command) == new_command diff --git a/thefuck/rules/composer_not_command.py b/thefuck/rules/composer_not_command.py index bfe4c5a4..6addd15f 100644 --- a/thefuck/rules/composer_not_command.py +++ b/thefuck/rules/composer_not_command.py @@ -4,13 +4,34 @@ from thefuck.utils import replace_argument, for_app @for_app('composer') def match(command): - return (('did you mean this?' in command.output.lower() - or 'did you mean one of these?' in command.output.lower())) + # determine error type + # matching "did you mean this" is not enough as composer also gives spelling suggestions for mistakes other than mispelled commands + is_undefined_command_error = r"[Symfony\Component\Console\Exception\CommandNotFoundException]" in command.output + suggestions_present = (('did you mean this?' in command.output.lower() + or 'did you mean one of these?' in command.output.lower())) + return is_undefined_command_error and suggestions_present def get_new_command(command): - broken_cmd = re.findall(r"Command \"([^']*)\" is not defined", command.output)[0] - new_cmd = re.findall(r'Did you mean this\?[^\n]*\n\s*([^\n]*)', command.output) - if not new_cmd: - new_cmd = re.findall(r'Did you mean one of these\?[^\n]*\n\s*([^\n]*)', command.output) - return replace_argument(command.script, broken_cmd, new_cmd[0].strip()) + # since the command class already tells us the original argument, we need not resort to regex + broken_cmd = command.script_parts[1] + one_suggestion_only = 'did you mean this?' in command.output.lower() + if one_suggestion_only: + new_cmd = re.findall(r'Did you mean this\?[^\n]*\n\s*([^\n]*)', command.output) + return replace_argument(command.script, broken_cmd, new_cmd[0].strip()) + else: + # there are multiple suggestions + # trim output text to make it more digestable by regex + trim_start_index = command.output.find("Did you mean one of these?") + short_output = command.output[trim_start_index:] + stripped_lines = [line.strip() for line in short_output.split("\n")] + # each of the suggested commands can be found from index 1 to the first occurence of blank string + try: + end_index = stripped_lines.index('') + except ValueError: + end_index = None + suggested_commands = stripped_lines[1:end_index] + return [ + replace_argument(command.script, broken_cmd, cmd.strip()) + for cmd in suggested_commands + ] diff --git a/thefuck/rules/composer_not_package.py b/thefuck/rules/composer_not_package.py new file mode 100644 index 00000000..75f2bb68 --- /dev/null +++ b/thefuck/rules/composer_not_package.py @@ -0,0 +1,42 @@ +import re +from thefuck.utils import replace_argument, for_app + + +@for_app('composer') +def match(command): + # determine error type + # matching "did you mean this" is not enough as composer also gives spelling suggestions for mistakes other than mispelled commands + is_invalid_argument_exception = r"[InvalidArgumentException]" in command.output + is_unable_to_find_package = re.search(r"Could not find package (.*)\.", command.output) is not None + suggestions_present = (('did you mean this?' in command.output.lower() + or 'did you mean one of these?' in command.output.lower())) + return is_invalid_argument_exception and is_unable_to_find_package and suggestions_present + + +def get_new_command(command): + # because composer lets you install many packages at once, must look at output to determine the erroneous package name + wrong_package_name = re.search(r"Could not find package (.*)\.", command.output).group(1) + offending_script_param = wrong_package_name if (wrong_package_name in command.script_parts) else re.findall( + r"{}:[^ ]+".format(wrong_package_name), command.script)[0] + version_constraint = offending_script_param[len(wrong_package_name):] + one_suggestion_only = 'did you mean this?' in command.output.lower() + if one_suggestion_only: + # wrong regex?? + new_cmd = re.findall(r'Did you mean this\?[^\n]*\n\s*([^\n]*)', command.output) + return replace_argument(command.script, offending_script_param, new_cmd[0].strip() + version_constraint) + else: + # there are multiple suggestions + # trim output text to make it more digestable by regex + trim_start_index = command.output.find("Did you mean one of these?") + short_output = command.output[trim_start_index:] + stripped_lines = [line.strip() for line in short_output.split("\n")] + # each of the suggested commands can be found from index 1 to the first occurence of blank string + try: + end_index = stripped_lines.index('') + except ValueError: + end_index = None + suggested_commands = stripped_lines[1:end_index] + return [ + replace_argument(command.script, offending_script_param, cmd + version_constraint) + for cmd in suggested_commands + ]