diff --git a/phpcs.py b/phpcs.py index 7a18f4f..92637d2 100644 --- a/phpcs.py +++ b/phpcs.py @@ -312,6 +312,35 @@ def get_standards_available(self): class Fixer(ShellCommand): """Concrete class for PHP-CS-Fixer""" + # Config file names that php-cs-fixer searches for, in priority order. + CONFIG_FILES = [ + ".php-cs-fixer.php", + ".php-cs-fixer.dist.php", + ".php_cs", + ".php_cs.dist", + ] + + def find_config_dir(self, start_dir): + """Walk up from start_dir looking for a php-cs-fixer config file. + + Returns the directory containing the config file, or None if not found. + """ + current = os.path.normpath(start_dir) + while True: + for config_file in self.CONFIG_FILES: + if os.path.isfile(os.path.join(current, config_file)): + debug_message( + "Found php-cs-fixer config: " + + os.path.join(current, config_file) + ) + return current + parent = os.path.dirname(current) + if parent == current: + # Reached filesystem root without finding a config file. + break + current = parent + return None + def execute(self, path): args = [] @@ -336,9 +365,13 @@ def execute(self, path): target = os.path.normpath(path) - # Set the working directory to the target file's directory, allowing - # php-cs-fixer the opportunity to find a config file (e.g. .php_cs) via relative paths. - self.setWorkingDir(os.path.dirname(target)) + # Walk up from the target file's directory to find a php-cs-fixer config + # file. If found, use that directory as the working directory so + # php-cs-fixer picks up the config automatically. Otherwise, fall back + # to the target file's directory. + file_dir = os.path.dirname(target) + config_dir = self.find_config_dir(file_dir) + self.setWorkingDir(config_dir if config_dir else file_dir) args.append("fix") args.append(target) diff --git a/tests/test_phpcs.py b/tests/test_phpcs.py index a7780d1..2287a7a 100644 --- a/tests/test_phpcs.py +++ b/tests/test_phpcs.py @@ -156,8 +156,9 @@ def test_sniffer_sets_working_dir(self, _): sniffer.execute(self.test_path) self.assertEqual(self.expected_dir, sniffer.workingDir) + @patch("Phpcs.phpcs.os.path.isfile", return_value=False) @patch("Phpcs.phpcs.Fixer.shell_out", return_value="") - def test_fixer_sets_working_dir(self, _): + def test_fixer_sets_working_dir_to_file_dir_when_no_config(self, _, __): s = sublime.load_settings("phpcs.sublime-settings") s.set("php_cs_fixer_executable_path", "/usr/bin/php-cs-fixer") s.set("phpcs_php_prefix_path", "") @@ -167,6 +168,28 @@ def test_fixer_sets_working_dir(self, _): fixer.execute(self.test_path) self.assertEqual(self.expected_dir, fixer.workingDir) + @patch("Phpcs.phpcs.Fixer.shell_out", return_value="") + def test_fixer_sets_working_dir_to_config_dir(self, _): + """When a config file exists in a parent directory, cwd should be set there.""" + s = sublime.load_settings("phpcs.sublime-settings") + s.set("php_cs_fixer_executable_path", "/usr/bin/php-cs-fixer") + s.set("phpcs_php_prefix_path", "") + s.set("php_cs_fixer_additional_args", {}) + + # The test path is /home/user/projects/myapp/src/Controller.php + # Simulate a config file at /home/user/projects/myapp/.php-cs-fixer.php + config_path = os.path.normpath("/home/user/projects/myapp/.php-cs-fixer.php") + + def fake_isfile(path): + return os.path.normpath(path) == config_path + + with patch("Phpcs.phpcs.os.path.isfile", side_effect=fake_isfile): + fixer = Fixer() + fixer.execute(self.test_path) + self.assertEqual( + os.path.normpath("/home/user/projects/myapp"), fixer.workingDir + ) + @patch("Phpcs.phpcs.CodeBeautifier.shell_out", return_value="") def test_code_beautifier_sets_working_dir(self, _): s = sublime.load_settings("phpcs.sublime-settings") @@ -267,3 +290,76 @@ def test_returns_value_unchanged_when_no_window(self): with patch("Phpcs.phpcs.sublime.active_window", return_value=None): result = self.pref._expand_variables("${project_path}/.php-cs-fixer.php") self.assertEqual("${project_path}/.php-cs-fixer.php", result) + + +class TestFixerFindConfigDir(TestCase): + """Test that Fixer.find_config_dir walks up the directory tree correctly.""" + + def setUp(self): + self.fixer = Fixer() + + def test_returns_none_when_no_config_found(self): + with patch("Phpcs.phpcs.os.path.isfile", return_value=False): + result = self.fixer.find_config_dir("/home/user/projects/myapp/src") + self.assertIsNone(result) + + def test_finds_config_in_start_dir(self): + config_path = os.path.normpath( + "/home/user/projects/myapp/src/.php-cs-fixer.php" + ) + + def fake_isfile(path): + return os.path.normpath(path) == config_path + + with patch("Phpcs.phpcs.os.path.isfile", side_effect=fake_isfile): + result = self.fixer.find_config_dir("/home/user/projects/myapp/src") + self.assertEqual(os.path.normpath("/home/user/projects/myapp/src"), result) + + def test_finds_config_in_parent_dir(self): + config_path = os.path.normpath("/home/user/projects/myapp/.php-cs-fixer.php") + + def fake_isfile(path): + return os.path.normpath(path) == config_path + + with patch("Phpcs.phpcs.os.path.isfile", side_effect=fake_isfile): + result = self.fixer.find_config_dir("/home/user/projects/myapp/src") + self.assertEqual(os.path.normpath("/home/user/projects/myapp"), result) + + def test_finds_dist_config(self): + config_path = os.path.normpath( + "/home/user/projects/myapp/.php-cs-fixer.dist.php" + ) + + def fake_isfile(path): + return os.path.normpath(path) == config_path + + with patch("Phpcs.phpcs.os.path.isfile", side_effect=fake_isfile): + result = self.fixer.find_config_dir("/home/user/projects/myapp/src") + self.assertEqual(os.path.normpath("/home/user/projects/myapp"), result) + + def test_finds_legacy_php_cs_config(self): + config_path = os.path.normpath("/home/user/projects/myapp/.php_cs") + + def fake_isfile(path): + return os.path.normpath(path) == config_path + + with patch("Phpcs.phpcs.os.path.isfile", side_effect=fake_isfile): + result = self.fixer.find_config_dir("/home/user/projects/myapp/src") + self.assertEqual(os.path.normpath("/home/user/projects/myapp"), result) + + def test_prefers_php_cs_fixer_php_over_dist(self): + """When both .php-cs-fixer.php and .php-cs-fixer.dist.php exist, + the directory with .php-cs-fixer.php should be returned first + since CONFIG_FILES lists it first.""" + primary_path = os.path.normpath( + "/home/user/projects/myapp/src/.php-cs-fixer.php" + ) + dist_path = os.path.normpath("/home/user/projects/myapp/.php-cs-fixer.dist.php") + + def fake_isfile(path): + return os.path.normpath(path) in (primary_path, dist_path) + + with patch("Phpcs.phpcs.os.path.isfile", side_effect=fake_isfile): + result = self.fixer.find_config_dir("/home/user/projects/myapp/src") + # Should find the config in src/ first (closer to the file) + self.assertEqual(os.path.normpath("/home/user/projects/myapp/src"), result)