diff --git a/PHPCSUtils/Utils/FilePath.php b/PHPCSUtils/Utils/FilePath.php new file mode 100644 index 00000000..f08651ff --- /dev/null +++ b/PHPCSUtils/Utils/FilePath.php @@ -0,0 +1,154 @@ +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); + } +} diff --git a/Tests/Utils/FilePath/GetNameTest.php b/Tests/Utils/FilePath/GetNameTest.php new file mode 100644 index 00000000..4d47391c --- /dev/null +++ b/Tests/Utils/FilePath/GetNameTest.php @@ -0,0 +1,141 @@ +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 .= 'assertSame($expected, FilePath::getName($phpcsFile)); + } + + /** + * Data provider. + * + * @see testGetName() For the array format. + * + * @return array> + */ + 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', + ], + ]; + } +} diff --git a/Tests/Utils/FilePath/IsStdinTest.php b/Tests/Utils/FilePath/IsStdinTest.php new file mode 100644 index 00000000..50533d31 --- /dev/null +++ b/Tests/Utils/FilePath/IsStdinTest.php @@ -0,0 +1,113 @@ +sniffs = ['Dummy.Dummy.Dummy']; // Limiting it to just one (dummy) sniff. + self::$config->cache = false; + + self::$ruleset = new Ruleset(self::$config); + } + + /** + * Test checking whether the input comes from STDIN or not. + * + * @dataProvider dataIsStdin + * + * @param string $fileName The file name to pass. + * @param bool $expected The expected function return value. + * + * @return void + */ + public function testIsStdin($fileName, $expected) + { + $content = 'phpcs_input_file: ' . $fileName . \PHP_EOL; + $content .= 'assertSame($expected, FilePath::isStdin($phpcsFile)); + } + + /** + * Data provider. + * + * @see testIsStdin() For the array format. + * + * @return array> + */ + public static function dataIsStdin() + { + return [ + 'path is dot' => [ + 'fileName' => '.', + 'expected' => false, + ], + 'path to file' => [ + 'fileName' => 'my/path/to/', + 'expected' => false, + ], + 'path is stdin' => [ + 'fileName' => 'STDIN', + 'expected' => true, + ], + 'path is stdin (single-quoted)' => [ + 'fileName' => "'STDIN'", + 'expected' => true, + ], + 'path is stdin (double-quoted)' => [ + 'fileName' => '"STDIN"', + 'expected' => true, + ], + ]; + } +} diff --git a/Tests/Utils/FilePath/NormalizeDirectorySeparatorsTest.php b/Tests/Utils/FilePath/NormalizeDirectorySeparatorsTest.php new file mode 100644 index 00000000..0d6cc999 --- /dev/null +++ b/Tests/Utils/FilePath/NormalizeDirectorySeparatorsTest.php @@ -0,0 +1,69 @@ +assertSame($expected, FilePath::normalizeDirectorySeparators($input)); + } + + /** + * Data provider. + * + * @see testNormalizeDirectorySeparators() For the array format. + * + * @return array> + */ + public static function dataNormalizeDirectorySeparators() + { + return [ + 'path is dot' => [ + 'input' => '.', + 'expected' => '.', + ], + 'path containing forward slashes only' => [ + 'input' => 'my/path/to/', + 'expected' => 'my/path/to/', + ], + 'path containing back-slashes only' => [ + 'input' => 'my\path\to\\', + 'expected' => 'my/path/to/', + ], + 'path containing a mix of forward and backslashes' => [ + 'input' => 'my\path/to\\', + 'expected' => 'my/path/to/', + ], + ]; + } +} diff --git a/Tests/Utils/FilePath/NormalizePathTest.php b/Tests/Utils/FilePath/NormalizePathTest.php new file mode 100644 index 00000000..b8310ed7 --- /dev/null +++ b/Tests/Utils/FilePath/NormalizePathTest.php @@ -0,0 +1,89 @@ +assertSame($absolute, FilePath::normalizeAbsolutePath($input)); + } + + /** + * Data provider. + * + * @see testNormalizeAbsolutePath() For the array format. + * + * @return array> + */ + public static function dataNormalizePath() + { + return [ + 'path is dot' => [ + 'input' => '.', + 'absolute' => './', + ], + 'path containing forward slashes only with trailing slash' => [ + 'input' => 'my/path/to/', + 'absolute' => 'my/path/to/', + ], + 'path containing forward slashes only without trailing slash' => [ + 'input' => 'my/path/to', + 'absolute' => 'my/path/to/', + ], + 'path containing forward slashes only with leading and trailing slash' => [ + 'input' => '/my/path/to/', + 'absolute' => '/my/path/to/', + ], + 'path containing back-slashes only with trailing slash' => [ + 'input' => 'my\path\to\\', + 'absolute' => 'my/path/to/', + ], + 'path containing back-slashes only without trailing slash' => [ + 'input' => 'my\path\to', + 'absolute' => 'my/path/to/', + ], + 'path containing back-slashes only with leading, no trailing slash' => [ + 'input' => '\my\path\to', + 'absolute' => '/my/path/to/', + ], + 'path containing a mix of forward and backslashes with leading and trailing slash' => [ + 'input' => '/my\path/to\\', + 'absolute' => '/my/path/to/', + ], + 'path containing a mix of forward and backslashes without trailing slash' => [ + 'input' => 'my\path/to', + 'absolute' => 'my/path/to/', + ], + ]; + } +} diff --git a/Tests/Utils/FilePath/StartsWithTest.php b/Tests/Utils/FilePath/StartsWithTest.php new file mode 100644 index 00000000..e6dd32e6 --- /dev/null +++ b/Tests/Utils/FilePath/StartsWithTest.php @@ -0,0 +1,99 @@ +assertSame($expected, FilePath::startsWith($haystack, $needle)); + } + + /** + * Data provider. + * + * @see testStartsWith() For the array format. + * + * @return array> + */ + public static function dataStartsWith() + { + return [ + 'path equal to other path, forward slashes' => [ + 'haystack' => '/my/path/to/', + 'needle' => '/my/path/to/', + 'expected' => true, + ], + 'path starting with other path, forward slashes' => [ + 'haystack' => '/my/path/to/some/sub/directory', + 'needle' => '/my/path/to/', + 'expected' => true, + ], + 'path equal to other path, back slashes' => [ + 'haystack' => 'C:\my\path\to\\', + 'needle' => 'C:\my\path\to\\', + 'expected' => true, + ], + 'path starting with other path, back slashes' => [ + 'haystack' => 'C:\my\path\to\some\sub\directory', + 'needle' => 'C:\my\path\to\\', + 'expected' => true, + ], + 'path starting with other path, but slashes are different' => [ + 'haystack' => '\my\path\to\some\sub\directory', + 'needle' => '/my/path/to/', + 'expected' => false, + ], + 'path starting with other path, but case is different' => [ + 'haystack' => '/My/path/To/some/Sub/directory', + 'needle' => '/my/path/to/', + 'expected' => false, + ], + 'path NOT starting with other path, forward slashes' => [ + 'haystack' => '/my/path/too/some/sub/directory', + 'needle' => '/my/path/to/', + 'expected' => false, + ], + 'path NOT starting with other path, back slashes' => [ + 'haystack' => 'C:\my\path\too\some\sub\directory', + 'needle' => 'C:\my\path\to\\', + 'expected' => false, + ], + 'completely different paths' => [ + 'haystack' => '/your/subdir/', + 'needle' => 'my/path/to/', + 'expected' => false, + ], + ]; + } +} diff --git a/Tests/Utils/FilePath/TrailingSlashItTest.php b/Tests/Utils/FilePath/TrailingSlashItTest.php new file mode 100644 index 00000000..6a56bc0d --- /dev/null +++ b/Tests/Utils/FilePath/TrailingSlashItTest.php @@ -0,0 +1,93 @@ +assertSame($expected, FilePath::trailingSlashIt($input)); + } + + /** + * Data provider. + * + * @see testTrailingSlashIt() For the array format. + * + * @return array> + */ + public static function dataTrailingSlashIt() + { + return [ + 'path is non-string' => [ + 'input' => false, + 'expected' => '', + ], + 'path is empty string' => [ + 'input' => '', + 'expected' => '', + ], + 'path is dot' => [ + 'input' => '.', + 'expected' => './', + ], + 'path with trailing forward slash' => [ + 'input' => 'my/path/to/', + 'expected' => 'my/path/to/', + ], + 'path with trailing back slash' => [ + 'input' => 'my\path\to\\', + 'expected' => 'my\path\to/', + ], + 'path without trailing slash' => [ + 'input' => 'my/path/to', + 'expected' => 'my/path/to/', + ], + 'path to a file with an extension' => [ + 'input' => 'my/path/to/filename.ext', + 'expected' => 'my/path/to/filename.ext', + ], + 'path to a dot file' => [ + 'input' => 'my/path/to/.gitignore', + 'expected' => 'my/path/to/.gitignore', + ], + 'path ending on a dot' => [ + 'input' => 'my/path/to/dot.', + 'expected' => 'my/path/to/dot./', + ], + 'path with trailing forward slash and the last dir contains a dot' => [ + 'input' => 'my/path/to.ext/', + 'expected' => 'my/path/to.ext/', + ], + ]; + } +}