Page MenuHomePhabricator

D9678.id23226.diff
No OneTemporary

D9678.id23226.diff

diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -153,6 +153,8 @@
'PhutilDocblockParserTestCase' => 'parser/__tests__/PhutilDocblockParserTestCase.php',
'PhutilEditDistanceMatrix' => 'utils/PhutilEditDistanceMatrix.php',
'PhutilEditDistanceMatrixTestCase' => 'utils/__tests__/PhutilEditDistanceMatrixTestCase.php',
+ 'PhutilEditorConfig' => 'parser/PhutilEditorConfig.php',
+ 'PhutilEditorConfigTestCase' => 'parser/__tests__/PhutilEditorConfigTestCase.php',
'PhutilEmailAddress' => 'parser/PhutilEmailAddress.php',
'PhutilEmailAddressTestCase' => 'parser/__tests__/PhutilEmailAddressTestCase.php',
'PhutilErrorHandler' => 'error/PhutilErrorHandler.php',
@@ -388,7 +390,9 @@
'phutil_get_library_root' => 'moduleutils/moduleutils.php',
'phutil_get_library_root_for_path' => 'moduleutils/moduleutils.php',
'phutil_get_signal_name' => 'future/exec/execx.php',
+ 'phutil_glob_to_regex' => 'utils/utils.php',
'phutil_implode_html' => 'markup/render.php',
+ 'phutil_ini_decode' => 'utils/utils.php',
'phutil_is_hiphop_runtime' => 'utils/utils.php',
'phutil_is_utf8' => 'utils/utf8.php',
'phutil_is_utf8_slowly' => 'utils/utf8.php',
@@ -565,6 +569,7 @@
'PhutilDirectedScalarGraph' => 'AbstractDirectedGraph',
'PhutilDocblockParserTestCase' => 'PhutilTestCase',
'PhutilEditDistanceMatrixTestCase' => 'PhutilTestCase',
+ 'PhutilEditorConfigTestCase' => 'PhutilTestCase',
'PhutilEmailAddressTestCase' => 'PhutilTestCase',
'PhutilErrorHandlerTestCase' => 'PhutilTestCase',
'PhutilErrorTrap' => 'Phobject',
diff --git a/src/parser/PhutilEditorConfig.php b/src/parser/PhutilEditorConfig.php
new file mode 100644
--- /dev/null
+++ b/src/parser/PhutilEditorConfig.php
@@ -0,0 +1,89 @@
+<?php
+
+/**
+ * Utilities for parsing [[http://editorconfig.org/ | EditorConfig]] files.
+ */
+final class PhutilEditorConfig {
+
+ private $configs;
+ private $rootPath;
+
+ /**
+ * Constructor.
+ *
+ * @param string The root directory.
+ */
+ public function __construct($root) {
+ $this->rootPath = $root;
+
+ $paths = id(new FileFinder($root))
+ ->withType('f')
+ ->withName('.editorconfig')
+ ->find();
+
+ foreach ($paths as $path) {
+ $this->addEditorConfigFile(Filesystem::resolvePath($path, $root));
+ }
+ }
+
+ protected function addEditorConfigFile($path) {
+ $file = Filesystem::readFile($path);
+ $config = phutil_ini_decode($file);
+
+ unset($config['root']);
+ $this->validateEditorConfig($config);
+
+ $path = dirname($path);
+ $this->configs[$path] = array();
+
+ foreach ($config as $key => $value) {
+ if (preg_match('@/@', $key)) {
+ $this->configs[$path][phutil_glob_to_regex('**/'.$key)] = $value;
+ } else if (preg_match('@^/@', $key)) {
+ $this->configs[$path][phutil_glob_to_regex(substr($key, 1))] = $value;
+ } else {
+ $this->configs[$path][phutil_glob_to_regex($key)] = $value;
+ }
+ }
+ }
+
+ public static function validateEditorConfig(array $config) {
+ $spec = PhutilTypeSpec::newFromString('map<string, map<string, string>>');
+ $spec->check($config);
+ }
+
+ /**
+ * Return the paths of all EditorConfig files that were found.
+ *
+ * @return list<string>
+ */
+ public function getPaths() {
+ $paths = array();
+
+ foreach (array_keys($this->configs) as $path) {
+ $paths[] = $path.'/.editorconfig';
+ }
+
+ return $paths;
+ }
+
+ /**
+ * Return the root path.
+ *
+ * @return string
+ */
+ public function getRootPath() {
+ return $this->rootPath;
+ }
+
+ public function getConfig($path, $key) {
+ foreach ($this->configs as $config_path => $config) {
+ if (Filesystem::isDescendant($path, $config_path)) {
+ foreach ($config as $config_key => $config_value) {
+
+ }
+ }
+ }
+ }
+
+}
diff --git a/src/parser/__tests__/PhutilEditorConfigTestCase.php b/src/parser/__tests__/PhutilEditorConfigTestCase.php
new file mode 100644
--- /dev/null
+++ b/src/parser/__tests__/PhutilEditorConfigTestCase.php
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * @group testcase
+ */
+final class PhutilEditorConfigTestCase extends PhutilTestCase {
+
+ public function testGetPaths() {
+ $tests = array(
+ dirname(__FILE__) => array(
+ dirname(__FILE__).'/editorconfig/.editorconfig',
+ dirname(__FILE__).'/editorconfig/subdir1/.editorconfig',
+ dirname(__FILE__).'/editorconfig/subdir2/.editorconfig',
+ ),
+ dirname(__FILE__).'/editorconfig/subdir1' => array(
+ dirname(__FILE__).'/editorconfig/subdir1/.editorconfig',
+ ),
+ dirname(__FILE__).'/editorconfig/subdir2' => array(
+ dirname(__FILE__).'/editorconfig/subdir2/.editorconfig',
+ ),
+ dirname(__FILE__).'/../../../' => array(
+ Filesystem::resolvePath('.editorconfig', dirname(__FILE__).'/../../..'),
+ dirname(__FILE__).'/editorconfig/.editorconfig',
+ dirname(__FILE__).'/editorconfig/subdir1/.editorconfig',
+ dirname(__FILE__).'/editorconfig/subdir2/.editorconfig',
+ ),
+ );
+
+ foreach ($tests as $input => $expect) {
+ $editorconfig = new PhutilEditorConfig($input);
+ $this->assertEqual($expect, $editorconfig->getPaths());
+ }
+ }
+
+ public function testGetRootPath() {
+ $tests = array(
+ dirname(__FILE__) => dirname(__FILE__),
+ dirname(__FILE__).'/editorconfig/subdir1'
+ => dirname(__FILE__).'/editorconfig/subdir1',
+ dirname(__FILE__).'/editorconfig/subdir2'
+ => dirname(__FILE__).'/editorconfig/subdir2',
+ );
+
+ foreach ($tests as $input => $expect) {
+ $editorconfig = new PhutilEditorConfig($input);
+ $this->assertEqual($expect, $editorconfig->getRootPath());
+ }
+ }
+
+}
diff --git a/src/parser/__tests__/editorconfig/.editorconfig b/src/parser/__tests__/editorconfig/.editorconfig
new file mode 100644
diff --git a/src/parser/__tests__/editorconfig/subdir1/.editorconfig b/src/parser/__tests__/editorconfig/subdir1/.editorconfig
new file mode 100644
diff --git a/src/parser/__tests__/editorconfig/subdir2/.editorconfig b/src/parser/__tests__/editorconfig/subdir2/.editorconfig
new file mode 100644
--- /dev/null
+++ b/src/parser/__tests__/editorconfig/subdir2/.editorconfig
@@ -0,0 +1 @@
+root = true
diff --git a/src/utils/__tests__/PhutilUtilsTestCase.php b/src/utils/__tests__/PhutilUtilsTestCase.php
--- a/src/utils/__tests__/PhutilUtilsTestCase.php
+++ b/src/utils/__tests__/PhutilUtilsTestCase.php
@@ -544,6 +544,35 @@
}
}
+ public function testPhutilINIDecode() {
+ $valid_cases = array(
+ '' => array(),
+ "[foo]\nbar = baz\n" => array('foo' => array('bar' => 'baz')),
+ "; Comment\n[foo]\nbar = baz\n" => array('foo' => array('bar' => 'baz')),
+ "# Comment\n[foo]\nbar = baz\n" => array('foo' => array('bar' => 'baz')),
+ );
+
+ foreach ($valid_cases as $input => $expect) {
+ $result = phutil_ini_decode($input);
+ $this->assertEqual($expect, $result, 'phutil_ini_decode('.$input.')');
+ }
+
+ $invalid_cases = array(
+ '[',
+ "[\nfoo\n]\nbar = baz\n",
+ );
+
+ foreach ($invalid_cases as $input) {
+ $caught = null;
+ try {
+ phutil_ini_decode($input);
+ } catch (Exception $ex) {
+ $caught = $ex;
+ }
+ $this->assertTrue($caught instanceof Exception);
+ }
+ }
+
public function testCensorCredentials() {
$cases = array(
'' => '',
@@ -574,5 +603,72 @@
}
}
+ public function testGlobToRegex() {
+ $cases = array(
+ '' => array(
+ array(''),
+ array('f', '/'),
+ ),
+ '*' => array(
+ array('foo'),
+ array('foo/', '/foo'),
+ ),
+ '**' => array(
+ array('foo', 'foo/', '/foo', 'foo/bar/baz'),
+ array(),
+ ),
+ 'foo.*' => array(
+ array('foo.php', 'foo.a', 'foo.'),
+ array('fooo.php', 'foo.php/foo'),
+ ),
+ 'fo?' => array(
+ array('foo', 'fot'),
+ array('fooo', 'ffoo', 'fo/'),
+ ),
+ 'fo{o,t}' => array(
+ array('foo', 'fot'),
+ array('fob', 'fo/'),
+ ),
+ 'foo(bar|foo)' => array(
+ array('foo(bar|foo)'),
+ array('foobar', 'foofoo'),
+ ),
+ 'foo,bar' => array(
+ array('foo,bar'),
+ array('foo', 'bar'),
+ ),
+ 'fo{o,\\,}' => array(
+ array('foo', 'fo,'),
+ array(),
+ ),
+ 'fo{o,\\\\}' => array(
+ array('foo', 'fo\\'),
+ array(),
+ ),
+ '/foo' => array(
+ array('/foo'),
+ array('foo'),
+ ),
+ );
+
+ foreach ($cases as $input => $expect) {
+ list($matches, $no_matches) = $expect;
+
+ $regex = phutil_glob_to_regex($input);
+ PhutilTypeSpec::newFromString('regex')->check($regex);
+
+ foreach ($matches as $match) {
+ $this->assertTrue(
+ (bool)preg_match($regex, $match),
+ pht('Expecting "%s" to match "%s".', $regex, $match));
+ }
+
+ foreach ($no_matches as $no_match) {
+ $this->assertFalse(
+ (bool)preg_match($regex, $no_match),
+ pht('Expecting "%s" not to match "%s".', $regex, $no_match));
+ }
+ }
+ }
}
diff --git a/src/utils/utils.php b/src/utils/utils.php
--- a/src/utils/utils.php
+++ b/src/utils/utils.php
@@ -1065,6 +1065,30 @@
return $result;
}
+/**
+ * Decode an INI string.
+ *
+ * @param string
+ * @return mixed
+ */
+function phutil_ini_decode($string) {
+ if (function_exists('parse_ini_string')) {
+ $result = @parse_ini_string($string, true);
+ } else {
+ $tmp = new TempFile('ini');
+ Filesystem::writeFile($tmp, $string);
+ $full_path = (string)$tmp;
+
+ $result = @parse_ini_file($full_path, true);
+ }
+
+ if ($result === false) {
+ throw new Exception('Invalid INI.');
+ }
+
+ return $result;
+}
+
/**
* Attempt to censor any plaintext credentials from a string.
@@ -1080,3 +1104,65 @@
function phutil_censor_credentials($string) {
return preg_replace(',(?<=://)([^/@\s]+)(?=@|$),', 'xxxxx', $string);
}
+
+/**
+ * Returns a regular expression which is equivalent to the given glob pattern.
+ *
+ * This function was adapted from
+ * https://github.com/symfony/Finder/blob/master/Glob.php.
+ *
+ * @param string A glob pattern.
+ * @return regex
+ */
+function phutil_glob_to_regex($glob) {
+ $escaping = false;
+ $in_curlies = 0;
+ $regex = '';
+
+ for ($i = 0; $i < strlen($glob); $i++) {
+ $char = $glob[$i];
+ $next_char = ($i < strlen($glob) - 1) ? $glob[$i + 1] : null;
+
+ if (in_array($char, array('.', '(', ')', '|', '+', '^', '$'))) {
+ $regex .= "\\$char";
+ } else if ($char === '*') {
+ if ($escaping) {
+ $regex .= '\\*';
+ } else {
+ if ($next_char === '*') {
+ $regex .= '.*';
+ } else {
+ $regex .= '[^/]*';
+ }
+ }
+ } else if ($char === '?') {
+ $regex .= $escaping ? '\\?' : '[^/]';
+ } else if ($char === '{') {
+ $regex .= $escaping ? '\\{' : '(';
+ if (!$escaping) {
+ ++$in_curlies;
+ }
+ } else if ($char === '}' && $in_curlies) {
+ $regex .= $escaping ? '}' : ')';
+ if (!$escaping) {
+ --$in_curlies;
+ }
+ } else if ($char === ',' && $in_curlies) {
+ $regex .= $escaping ? ',' : '|';
+ } else if ($char === '\\') {
+ if ($escaping) {
+ $regex .= '\\\\';
+ $escaping = false;
+ } else {
+ $escaping = true;
+ }
+
+ continue;
+ } else {
+ $regex .= $char;
+ }
+ $escaping = false;
+ }
+
+ return '#^'.$regex.'$#';
+}

File Metadata

Mime Type
text/plain
Expires
Fri, May 10, 3:10 PM (4 w, 8 h ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6283595
Default Alt Text
D9678.id23226.diff (11 KB)

Event Timeline