Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# CHANGELOG

## 7.2.0

- Support Sublime Text variable expansion (`${project_path}`, `${folder}`, etc.) in all setting values, enabling project-relative config file paths. Resolves [#143](https://github.com/benmatselby/sublime-phpcs/issues/143).
- Set the working directory to the target file's directory for Fixer, CodeBeautifier, MessDetector, and Linter, matching existing Sniffer behaviour. This allows relative paths in additional args to resolve correctly.
- Added unit tests for variable expansion and working directory behaviour.

## 7.1.0

- Provide the ability to have platform specific paths for the tools. Resolves [#183](https://github.com/benmatselby/sublime-phpcs/issues/183).
Expand Down
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,41 @@ If you use this plugin across multiple operating systems (e.g. syncing settings

This works for all path settings: `phpcs_executable_path`, `phpcs_php_prefix_path`, `phpcs_php_path`, `php_cs_fixer_executable_path`, `phpcbf_executable_path`, and `phpmd_executable_path`. Plain string values continue to work as before.

### Variable Expansion

All string settings support Sublime Text variable expansion using the `${var}` syntax. This is particularly useful for specifying project-relative config file paths. The most useful variables are:

- `$project_path` — The path to the folder containing the `.sublime-project` file
- `$folder` — The full path to the first folder listed in the side bar

For example, to point php-cs-fixer at a project-specific config file:

```json
{
"settings": {
"phpcs": {
"php_cs_fixer_additional_args": {
"--config": "${project_path}/.php-cs-fixer.php"
}
}
}
}
```

Or to use a project-local installation of phpcs:

```json
{
"settings": {
"phpcs": {
"phpcs_executable_path": "${folder}/vendor/bin/phpcs"
}
}
}
```

See the full list of available variables in the [Sublime Text documentation](https://www.sublimetext.com/docs/build_systems.html#variables).

## FAQ

### What do I do when I get "OSError: [Errno 8] Exec format error"?
Expand Down
52 changes: 47 additions & 5 deletions phpcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ def get(self, key):
raw = self.project_settings.get(key)
else:
raw = self.settings.get(key)
return self._resolve_platform_value(raw)
value = self._resolve_platform_value(raw)
return self._expand_variables(value)

def _resolve_platform_value(self, value):
"""
Expand All @@ -98,6 +99,22 @@ def _resolve_platform_value(self, value):
return ""
return value

def _expand_variables(self, value):
"""
Expand Sublime Text variables in setting values using the ${var} syntax.
The most useful variables are $project_path and $folder, which remain
stable within a project session. Other variables such as $file and
$file_path are available but may not reflect the current file when
settings are cached at load time.

See https://www.sublimetext.com/docs/build_systems.html#variables
"""
window = sublime.active_window()
if window is None:
return value
variables = window.extract_variables()
return sublime.expand_variables(value, variables)

def set(self, key, value):
if key in self.project_settings:
self.project_settings[key] = value
Expand Down Expand Up @@ -317,8 +334,14 @@ def execute(self, path):
)
return

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))

args.append("fix")
args.append(os.path.normpath(path))
args.append(target)
args.append("--verbose")

# Add the additional arguments from the settings file to the command
Expand Down Expand Up @@ -363,7 +386,13 @@ def execute(self, path):
)
return

args.append(os.path.normpath(path))
target = os.path.normpath(path)

# Set the working directory to the target file's directory, allowing
# phpcbf the opportunity to find config files via relative paths.
self.setWorkingDir(os.path.dirname(target))

args.append(target)

# Add the additional arguments from the settings file to the command
for key, value in pref.get("phpcbf_additional_args").items():
Expand Down Expand Up @@ -408,7 +437,13 @@ def execute(self, path):
else:
args = [application_path]

args.append(os.path.normpath(path))
target = os.path.normpath(path)

# Set the working directory to the target file's directory, allowing
# phpmd the opportunity to find config files via relative paths.
self.setWorkingDir(os.path.dirname(target))

args.append(target)
args.append("text")

for key, value in pref.get("phpmd_additional_args").items():
Expand Down Expand Up @@ -443,7 +478,14 @@ def execute(self, path):

args.append("-l")
args.append("-d display_errors=On")
args.append(os.path.normpath(path))

target = os.path.normpath(path)

# Set the working directory to the target file's directory for consistency
# with other tool commands.
self.setWorkingDir(os.path.dirname(target))

args.append(target)

self.parse_report(args)

Expand Down
11 changes: 11 additions & 0 deletions phpcs.sublime-settings
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
{
// Plugin settings
//
// All string settings support Sublime Text variable expansion using
// the ${var} syntax. The most useful variables are:
// $project_path - The path to the folder containing the .sublime-project file
// $folder - The full path to the first folder in the side bar
// See https://www.sublimetext.com/docs/build_systems.html#variables

// Turn the debug output on/off
"show_debug": false,
Expand Down Expand Up @@ -108,6 +114,11 @@
"php_cs_fixer_executable_path": "",

// Additional arguments you can specify into the application
//
// Supports ${var} variable expansion, for example:
// "php_cs_fixer_additional_args": {
// "--config": "${project_path}/.php-cs-fixer.php"
// }
"php_cs_fixer_additional_args": {},

// phpcbf settings
Expand Down
144 changes: 143 additions & 1 deletion tests/test_phpcs.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import os
from unittest import TestCase
from unittest.mock import patch

import sublime

from Phpcs.phpcs import Pref, Sniffer
from Phpcs.phpcs import (
CodeBeautifier,
Fixer,
Linter,
MessDetector,
Pref,
Sniffer,
)


class TestSniffer(TestCase):
Expand Down Expand Up @@ -125,3 +133,137 @@ def test_list_value_passes_through(self):
def test_boolean_value_passes_through(self):
result = self.pref._resolve_platform_value(True)
self.assertTrue(result)


class TestWorkingDirectory(TestCase):
"""Test that all tool classes set the working directory to the target file's directory."""

def setUp(self):
self.test_path = os.path.normpath(
"/home/user/projects/myapp/src/Controller.php"
)
self.expected_dir = os.path.dirname(self.test_path)

@patch("Phpcs.phpcs.Sniffer.shell_out", return_value="")
def test_sniffer_sets_working_dir(self, _):
s = sublime.load_settings("phpcs.sublime-settings")
s.set("phpcs_sniffer_run", True)
s.set("phpcs_executable_path", "/usr/bin/phpcs")
s.set("phpcs_php_prefix_path", "")
s.set("phpcs_additional_args", {})

sniffer = Sniffer()
sniffer.execute(self.test_path)
self.assertEqual(self.expected_dir, sniffer.workingDir)

@patch("Phpcs.phpcs.Fixer.shell_out", return_value="")
def test_fixer_sets_working_dir(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", "")
s.set("php_cs_fixer_additional_args", {})

fixer = Fixer()
fixer.execute(self.test_path)
self.assertEqual(self.expected_dir, 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")
s.set("phpcbf_executable_path", "/usr/bin/phpcbf")
s.set("phpcs_php_prefix_path", "")
s.set("phpcbf_additional_args", {})

beautifier = CodeBeautifier()
beautifier.execute(self.test_path)
self.assertEqual(self.expected_dir, beautifier.workingDir)

@patch("Phpcs.phpcs.MessDetector.shell_out", return_value="")
def test_mess_detector_sets_working_dir(self, _):
s = sublime.load_settings("phpcs.sublime-settings")
s.set("phpmd_run", True)
s.set("phpmd_executable_path", "/usr/bin/phpmd")
s.set("phpcs_php_prefix_path", "")
s.set("phpmd_additional_args", {})

detector = MessDetector()
detector.execute(self.test_path)
self.assertEqual(self.expected_dir, detector.workingDir)

@patch("Phpcs.phpcs.Linter.shell_out", return_value="")
def test_linter_sets_working_dir(self, _):
s = sublime.load_settings("phpcs.sublime-settings")
s.set("phpcs_linter_run", True)
s.set("phpcs_php_path", "/usr/bin/php")
s.set("phpcs_linter_regex", "(?P<message>.*) on line (?P<line>\\d+)")

linter = Linter()
linter.execute(self.test_path)
self.assertEqual(self.expected_dir, linter.workingDir)


class TestPrefVariableExpansion(TestCase):
"""Test that Sublime Text variables are expanded in setting values."""

def setUp(self):
self.pref = Pref()
self.variables = {
"project_path": "/home/user/projects/myapp",
"folder": "/home/user/projects/myapp",
"file": "/home/user/projects/myapp/src/Controller.php",
"file_path": "/home/user/projects/myapp/src",
}

@patch("Phpcs.phpcs.sublime.active_window")
def test_string_with_project_path_is_expanded(self, mock_window):
mock_window.return_value.extract_variables.return_value = self.variables
result = self.pref._expand_variables("${project_path}/.php-cs-fixer.php")
self.assertEqual("/home/user/projects/myapp/.php-cs-fixer.php", result)

@patch("Phpcs.phpcs.sublime.active_window")
def test_string_with_folder_is_expanded(self, mock_window):
mock_window.return_value.extract_variables.return_value = self.variables
result = self.pref._expand_variables("${folder}/vendor/bin/phpcs")
self.assertEqual("/home/user/projects/myapp/vendor/bin/phpcs", result)

@patch("Phpcs.phpcs.sublime.active_window")
def test_string_without_variables_passes_through(self, mock_window):
mock_window.return_value.extract_variables.return_value = self.variables
result = self.pref._expand_variables("/usr/bin/phpcs")
self.assertEqual("/usr/bin/phpcs", result)

@patch("Phpcs.phpcs.sublime.active_window")
def test_empty_string_passes_through(self, mock_window):
mock_window.return_value.extract_variables.return_value = self.variables
result = self.pref._expand_variables("")
self.assertEqual("", result)

@patch("Phpcs.phpcs.sublime.active_window")
def test_dict_values_are_expanded_recursively(self, mock_window):
mock_window.return_value.extract_variables.return_value = self.variables
value = {"--config": "${project_path}/.php-cs-fixer.php", "-n": ""}
result = self.pref._expand_variables(value)
self.assertEqual(
{"--config": "/home/user/projects/myapp/.php-cs-fixer.php", "-n": ""},
result,
)

@patch("Phpcs.phpcs.sublime.active_window")
def test_boolean_value_passes_through(self, mock_window):
mock_window.return_value.extract_variables.return_value = self.variables
result = self.pref._expand_variables(True)
self.assertTrue(result)

@patch("Phpcs.phpcs.sublime.active_window")
def test_list_value_is_expanded_recursively(self, mock_window):
mock_window.return_value.extract_variables.return_value = self.variables
value = ["Sniffer", "${project_path}/vendor/bin/phpcs"]
result = self.pref._expand_variables(value)
self.assertEqual(
["Sniffer", "/home/user/projects/myapp/vendor/bin/phpcs"], result
)

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)