Skip to content

Commit

Permalink
✨ New FilePath class
Browse files Browse the repository at this point in the history
... 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.
  • Loading branch information
jrfnl committed May 13, 2024
1 parent c61fa32 commit da08c72
Show file tree
Hide file tree
Showing 7 changed files with 758 additions and 0 deletions.
154 changes: 154 additions & 0 deletions PHPCSUtils/Utils/FilePath.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<?php
/**
* PHPCSUtils, utility functions and classes for PHP_CodeSniffer sniff developers.
*
* @package PHPCSUtils
* @copyright 2019-2024 PHPCSUtils Contributors
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
* @link https://github.com/PHPCSStandards/PHPCSUtils
*/

namespace PHPCSUtils\Utils;

use PHP_CodeSniffer\Files\File;
use PHPCSUtils\Utils\TextStrings;

/**
* Helper functions for working with arbitrary file/directory paths.
*
* Typically, these methods are useful for sniffs which examine the name of the file
* under scan and need to act differently depending on the path in which the file
* under scan is found.
*
* @see \PHP_CodeSniffer\Files\getFilename Retrieves the absolute path to the file under scan.
* @see \PHPCSUtils\BackCompat\getCommandLineData Can be used to retrieve "basepath" setting.
*
* @since 1.1.0
*/
final class FilePath
{

/**
* Get the file name of the current file under scan.
*
* In contrast to the PHPCS native {@see \PHP_CodeSniffer\Files\getFilename()} method,
* the name returned by this method will have been normalized.
*
* @since 1.1.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
*
* @return string The file name without surrounding quotes and with forward slashes
* as directory separators.
*/
public static function getName(File $phpcsFile)
{
// Usage of `stripQuotes` is to ensure `stdin_path` passed by IDEs does not include quotes.
$fileName = TextStrings::stripQuotes($phpcsFile->getFileName());
if ($fileName !== 'STDIN') {
$fileName = self::normalizeAbsolutePath($fileName);
}

return \trim($fileName);
}

/**
* Check whether the input was received via STDIN.
*
* @since 1.1.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
*
* @return bool
*/
public static function isStdin(File $phpcsFile)
{
return (self::getName($phpcsFile) === 'STDIN');
}

/**
* Normalize an absolute path to forward slashes and to include a trailing slash for directories.
*
* @since 1.1.0
*
* @param string $path Absolute file or directory path.
*
* @return string
*/
public static function normalizeAbsolutePath($path)
{
return self::trailingSlashIt(self::normalizeDirectorySeparators($path));
}

/**
* Normalize all directory separators to be a forward slash.
*
* {@internal We cannot rely on the OS on which PHPCS is being run to determine the
* the expected slashes, as the file name could also come from a text string in a
* tokenized file or have been set by an IDE...}
*
* @since 1.1.0
*
* @param string $path File or directory path.
*
* @return string
*/
public static function normalizeDirectorySeparators($path)
{
return \strtr((string) $path, '\\', '/');
}

/**
* Ensure that a directory path ends on a trailing slash.
*
* Includes safeguard against adding a trailing slash to path ending on a file name.
*
* @since 1.1.0
*
* @param string $path File or directory path.
*
* @return string
*/
public static function trailingSlashIt($path)
{
if (\is_string($path) === false || $path === '') {
return '';
}

$extension = '';
$lastChar = \substr($path, -1);
if ($lastChar !== '/' && $lastChar !== '\\') {
// This may be a file, check if it has a file extension.
$extension = \pathinfo($path, \PATHINFO_EXTENSION);
}

if ($extension !== '') {
return $path;
}

return \rtrim((string) $path, '/\\') . '/';
}

/**
* Check whether one file/directory path starts with another path.
*
* Recommended to be used only when both paths are absolute.
*
* Note: this function does not normalize paths prior to comparing them.
* If this is needed, normalization should be done prior to passing
* the `$haystack` and `$needle` parameters to this function.
*
* Also note that this function does a case-sensitive comparison as most OS-es are case-sensitive.
*
* @since 1.1.0
*
* @param string $haystack Path to examine.
* @param string $needle Partial path which the haystack path should start with.
*
* @return bool
*/
public static function startsWith($haystack, $needle)
{
return (\strncmp($haystack, $needle, \strlen($needle)) === 0);
}
}
141 changes: 141 additions & 0 deletions Tests/Utils/FilePath/GetNameTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php
/**
* PHPCSUtils, utility functions and classes for PHP_CodeSniffer sniff developers.
*
* @package PHPCSUtils
* @copyright 2019-2024 PHPCSUtils Contributors
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
* @link https://github.com/PHPCSStandards/PHPCSUtils
*/

namespace PHPCSUtils\Tests\Utils\FilePath;

use PHP_CodeSniffer\Files\DummyFile;
use PHP_CodeSniffer\Ruleset;
use PHPCSUtils\TestUtils\ConfigDouble;
use PHPCSUtils\Utils\FilePath;
use PHPUnit\Framework\TestCase;

/**
* Test class.
*
* @covers \PHPCSUtils\Utils\FilePath::getName
*
* @since 1.1.0
*/
final class GetNameTest extends TestCase
{

/**
* Config object for use in the tests.
*
* @var \PHP_CodeSniffer\Config
*/
private static $config;

/**
* Ruleset object for use in the tests.
*
* @var \PHP_CodeSniffer\Ruleset
*/
private static $ruleset;

/**
* Initialize a PHPCS config and ruleset objects.
*
* @beforeClass
*
* @return void
*/
public static function setUpConfigRuleset()
{
parent::setUpBeforeClass();

self::$config = new ConfigDouble();
self::$config->sniffs = ['Dummy.Dummy.Dummy']; // Limiting it to just one (dummy) sniff.
self::$config->cache = false;

self::$ruleset = new Ruleset(self::$config);
}

/**
* Test retrieving the normalized file name.
*
* @dataProvider dataGetName
*
* @param string $fileName The file name to pass.
* @param string $expected The expected function return value.
*
* @return void
*/
public function testGetName($fileName, $expected)
{
$content = 'phpcs_input_file: ' . $fileName . \PHP_EOL;
$content .= '<?php ' . \PHP_EOL . '$var = FALSE;' . \PHP_EOL;

$phpcsFile = new DummyFile($content, self::$ruleset, self::$config);

$this->assertSame($expected, FilePath::getName($phpcsFile));
}

/**
* Data provider.
*
* @see testGetName() For the array format.
*
* @return array<string, array<string, string>>
*/
public static function dataGetName()
{
return [
'file path is empty string' => [
'fileName' => '',
'expected' => '',
],
'file path is stdin' => [
'fileName' => 'STDIN',
'expected' => 'STDIN',
],
'file path is stdin (single-quoted)' => [
'fileName' => "'STDIN'",
'expected' => 'STDIN',
],
'file path is stdin (double-quoted)' => [
'fileName' => '"STDIN"',
'expected' => 'STDIN',
],
'file path is dot' => [
'fileName' => '.',
'expected' => './',
],
'file path is file name only' => [
'fileName' => 'filename.php',
'expected' => 'filename.php',
],
'file path is file name only (single-quoted)' => [
'fileName' => "'filename.php'",
'expected' => 'filename.php',
],
'file path is file name only (double-quoted)' => [
'fileName' => '"filename.php"',
'expected' => 'filename.php',
],
'file path with forward slashes' => [
'fileName' => 'my/path/to/filename.php',
'expected' => 'my/path/to/filename.php',
],
'file path with backslashes' => [
'fileName' => 'my\path\to\filename.js',
'expected' => 'my/path/to/filename.js',
],
'file path containing a mix of forward and backslashes' => [
'fileName' => '/my\path/to\myfile.inc',
'expected' => '/my/path/to/myfile.inc',
],
'full windows file path, backslashes only' => [
'fileName' => 'C:\my\path\to\filename.css',
'expected' => 'C:/my/path/to/filename.css',
],
];
}
}
Loading

0 comments on commit da08c72

Please sign in to comment.