Skip to content

Commit 0b69380

Browse files
committed
✨ New FilePath class
... with some utility functions for handling file paths. The class initially contains the following utility methods: * `getName(File $phpcsFile): string` - Retrieve the normalized file name for the current file. * `isStdin(File $phpcsFile): bool` - Check whether the current file under scan comes from STDIN. * `normalizeAbsolutePath(string $path): string` - Normalize an absolute path to forward slashes and to include a trailing slash for directories. * `normalizeDirectorySeparators(string $path): string` - Normalize all directory separators to be a forward slash. * `trailingSlashIt(string $path): string` - Ensure that a directory path ends on a trailing slash. * `startsWith(string $haystack, string $needle): bool` - Check whether one file/directory path starts with another path. Includes unit tests.
1 parent 8e85c93 commit 0b69380

File tree

7 files changed

+758
-0
lines changed

7 files changed

+758
-0
lines changed

PHPCSUtils/Utils/FilePath.php

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
<?php
2+
/**
3+
* PHPCSUtils, utility functions and classes for PHP_CodeSniffer sniff developers.
4+
*
5+
* @package PHPCSUtils
6+
* @copyright 2019-2024 PHPCSUtils Contributors
7+
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
8+
* @link https://github.com/PHPCSStandards/PHPCSUtils
9+
*/
10+
11+
namespace PHPCSUtils\Utils;
12+
13+
use PHP_CodeSniffer\Files\File;
14+
use PHPCSUtils\Utils\TextStrings;
15+
16+
/**
17+
* Helper functions for working with arbitrary file/directory paths.
18+
*
19+
* Typically, these methods are useful for sniffs which examine the name of the file
20+
* under scan and need to act differently depending on the path in which the file
21+
* under scan is found.
22+
*
23+
* @see \PHP_CodeSniffer\Files\getFilename Retrieves the absolute path to the file under scan.
24+
* @see \PHPCSUtils\BackCompat\getCommandLineData Can be used to retrieve "basepath" setting.
25+
*
26+
* @since 1.1.0
27+
*/
28+
final class FilePath
29+
{
30+
31+
/**
32+
* Get the file name of the current file under scan.
33+
*
34+
* In contrast to the PHPCS native {@see \PHP_CodeSniffer\Files\getFilename()} method,
35+
* the name returned by this method will have been normalized.
36+
*
37+
* @since 1.1.0
38+
*
39+
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
40+
*
41+
* @return string The file name without surrounding quotes and with forward slashes
42+
* as directory separators.
43+
*/
44+
public static function getName(File $phpcsFile)
45+
{
46+
// Usage of `stripQuotes` is to ensure `stdin_path` passed by IDEs does not include quotes.
47+
$fileName = TextStrings::stripQuotes($phpcsFile->getFileName());
48+
if ($fileName !== 'STDIN') {
49+
$fileName = self::normalizeAbsolutePath($fileName);
50+
}
51+
52+
return \trim($fileName);
53+
}
54+
55+
/**
56+
* Check whether the input was received via STDIN.
57+
*
58+
* @since 1.1.0
59+
*
60+
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
61+
*
62+
* @return bool
63+
*/
64+
public static function isStdin(File $phpcsFile)
65+
{
66+
return (self::getName($phpcsFile) === 'STDIN');
67+
}
68+
69+
/**
70+
* Normalize an absolute path to forward slashes and to include a trailing slash for directories.
71+
*
72+
* @since 1.1.0
73+
*
74+
* @param string $path Absolute file or directory path.
75+
*
76+
* @return string
77+
*/
78+
public static function normalizeAbsolutePath($path)
79+
{
80+
return self::trailingSlashIt(self::normalizeDirectorySeparators($path));
81+
}
82+
83+
/**
84+
* Normalize all directory separators to be a forward slash.
85+
*
86+
* {@internal We cannot rely on the OS on which PHPCS is being run to determine the
87+
* the expected slashes, as the file name could also come from a text string in a
88+
* tokenized file or have been set by an IDE...}
89+
*
90+
* @since 1.1.0
91+
*
92+
* @param string $path File or directory path.
93+
*
94+
* @return string
95+
*/
96+
public static function normalizeDirectorySeparators($path)
97+
{
98+
return \strtr((string) $path, '\\', '/');
99+
}
100+
101+
/**
102+
* Ensure that a directory path ends on a trailing slash.
103+
*
104+
* Includes safeguard against adding a trailing slash to path ending on a file name.
105+
*
106+
* @since 1.1.0
107+
*
108+
* @param string $path File or directory path.
109+
*
110+
* @return string
111+
*/
112+
public static function trailingSlashIt($path)
113+
{
114+
if (\is_string($path) === false || $path === '') {
115+
return '';
116+
}
117+
118+
$extension = '';
119+
$lastChar = \substr($path, -1);
120+
if ($lastChar !== '/' && $lastChar !== '\\') {
121+
// This may be a file, check if it has a file extension.
122+
$extension = \pathinfo($path, \PATHINFO_EXTENSION);
123+
}
124+
125+
if ($extension !== '') {
126+
return $path;
127+
}
128+
129+
return \rtrim((string) $path, '/\\') . '/';
130+
}
131+
132+
/**
133+
* Check whether one file/directory path starts with another path.
134+
*
135+
* Recommended to be used only when both paths are absolute.
136+
*
137+
* Note: this function does not normalize paths prior to comparing them.
138+
* If this is needed, normalization should be done prior to passing
139+
* the `$haystack` and `$needle` parameters to this function.
140+
*
141+
* Also note that this function does a case-sensitive comparison as most OS-es are case-sensitive.
142+
*
143+
* @since 1.1.0
144+
*
145+
* @param string $haystack Path to examine.
146+
* @param string $needle Partial path which the haystack path should start with.
147+
*
148+
* @return bool
149+
*/
150+
public static function startsWith($haystack, $needle)
151+
{
152+
return (\strncmp($haystack, $needle, \strlen($needle)) === 0);
153+
}
154+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
<?php
2+
/**
3+
* PHPCSUtils, utility functions and classes for PHP_CodeSniffer sniff developers.
4+
*
5+
* @package PHPCSUtils
6+
* @copyright 2019-2024 PHPCSUtils Contributors
7+
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
8+
* @link https://github.com/PHPCSStandards/PHPCSUtils
9+
*/
10+
11+
namespace PHPCSUtils\Tests\Utils\FilePath;
12+
13+
use PHP_CodeSniffer\Files\DummyFile;
14+
use PHP_CodeSniffer\Ruleset;
15+
use PHPCSUtils\TestUtils\ConfigDouble;
16+
use PHPCSUtils\Utils\FilePath;
17+
use PHPUnit\Framework\TestCase;
18+
19+
/**
20+
* Test class.
21+
*
22+
* @covers \PHPCSUtils\Utils\FilePath::getName
23+
*
24+
* @since 1.1.0
25+
*/
26+
final class GetNameTest extends TestCase
27+
{
28+
29+
/**
30+
* Config object for use in the tests.
31+
*
32+
* @var \PHP_CodeSniffer\Config
33+
*/
34+
private static $config;
35+
36+
/**
37+
* Ruleset object for use in the tests.
38+
*
39+
* @var \PHP_CodeSniffer\Ruleset
40+
*/
41+
private static $ruleset;
42+
43+
/**
44+
* Initialize a PHPCS config and ruleset objects.
45+
*
46+
* @beforeClass
47+
*
48+
* @return void
49+
*/
50+
public static function setUpConfigRuleset()
51+
{
52+
parent::setUpBeforeClass();
53+
54+
self::$config = new ConfigDouble();
55+
self::$config->sniffs = ['Dummy.Dummy.Dummy']; // Limiting it to just one (dummy) sniff.
56+
self::$config->cache = false;
57+
58+
self::$ruleset = new Ruleset(self::$config);
59+
}
60+
61+
/**
62+
* Test retrieving the normalized file name.
63+
*
64+
* @dataProvider dataGetName
65+
*
66+
* @param string $fileName The file name to pass.
67+
* @param string $expected The expected function return value.
68+
*
69+
* @return void
70+
*/
71+
public function testGetName($fileName, $expected)
72+
{
73+
$content = 'phpcs_input_file: ' . $fileName . \PHP_EOL;
74+
$content .= '<?php ' . \PHP_EOL . '$var = FALSE;' . \PHP_EOL;
75+
76+
$phpcsFile = new DummyFile($content, self::$ruleset, self::$config);
77+
78+
$this->assertSame($expected, FilePath::getName($phpcsFile));
79+
}
80+
81+
/**
82+
* Data provider.
83+
*
84+
* @see testGetName() For the array format.
85+
*
86+
* @return array<string, array<string, string>>
87+
*/
88+
public static function dataGetName()
89+
{
90+
return [
91+
'file path is empty string' => [
92+
'fileName' => '',
93+
'expected' => '',
94+
],
95+
'file path is stdin' => [
96+
'fileName' => 'STDIN',
97+
'expected' => 'STDIN',
98+
],
99+
'file path is stdin (single-quoted)' => [
100+
'fileName' => "'STDIN'",
101+
'expected' => 'STDIN',
102+
],
103+
'file path is stdin (double-quoted)' => [
104+
'fileName' => '"STDIN"',
105+
'expected' => 'STDIN',
106+
],
107+
'file path is dot' => [
108+
'fileName' => '.',
109+
'expected' => './',
110+
],
111+
'file path is file name only' => [
112+
'fileName' => 'filename.php',
113+
'expected' => 'filename.php',
114+
],
115+
'file path is file name only (single-quoted)' => [
116+
'fileName' => "'filename.php'",
117+
'expected' => 'filename.php',
118+
],
119+
'file path is file name only (double-quoted)' => [
120+
'fileName' => '"filename.php"',
121+
'expected' => 'filename.php',
122+
],
123+
'file path with forward slashes' => [
124+
'fileName' => 'my/path/to/filename.php',
125+
'expected' => 'my/path/to/filename.php',
126+
],
127+
'file path with backslashes' => [
128+
'fileName' => 'my\path\to\filename.js',
129+
'expected' => 'my/path/to/filename.js',
130+
],
131+
'file path containing a mix of forward and backslashes' => [
132+
'fileName' => '/my\path/to\myfile.inc',
133+
'expected' => '/my/path/to/myfile.inc',
134+
],
135+
'full windows file path, backslashes only' => [
136+
'fileName' => 'C:\my\path\to\filename.css',
137+
'expected' => 'C:/my/path/to/filename.css',
138+
],
139+
];
140+
}
141+
}

0 commit comments

Comments
 (0)