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 @@ -150,6 +150,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', 'PhutilEmptyAuthAdapter' => 'auth/PhutilEmptyAuthAdapter.php', @@ -587,6 +589,7 @@ 'PhutilDisqusAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilDocblockParserTestCase' => 'PhutilTestCase', 'PhutilEditDistanceMatrixTestCase' => 'PhutilTestCase', + 'PhutilEditorConfigTestCase' => 'PhutilTestCase', 'PhutilEmailAddressTestCase' => 'PhutilTestCase', 'PhutilEmptyAuthAdapter' => 'PhutilAuthAdapter', 'PhutilErrorHandlerTestCase' => 'PhutilTestCase', 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,156 @@ + array('latin1', 'utf-8', 'utf-8-bom', 'utf-16be', 'utf-16le'), + 'end_of_line' => array('lf', 'cr', 'crlf'), + 'indent_size' => 'int|string', // integer or "tab" + 'indent_style' => array('space', 'tab'), + 'insert_final_newline' => 'bool', + 'max_line_length' => 'int', + 'tab_width' => 'int', + 'trim_trailing_whitespace' => 'bool', + ); + + private $root; + + /** + * Constructor. + * + * @param string The root directory. + */ + public function __construct($root) { + $this->root = $root; + } + + /** + * Get the specified EditorConfig property for the specified path. + * + * @param string + * @param string + * @return wild + */ + public function getProperty($path, $key) { + if (idx(self::$knownProperties, $key) === null) { + throw new InvalidArgumentException(pht('Invalid EditorConfig property.')); + } + + $props = $this->getProperties($path); + + switch ($key) { + case 'indent_size': + if (idx($props, 'indent_size') === null && + idx($props, 'indent_style') === 'tab') { + return 'tab'; + } else if (idx($props, 'indent_size') == 'tab' && + idx($props, 'tab_size') !== null) { + return idx($props, 'tab_size'); + } + break; + + case 'tab_width': + if (idx($props, 'indent_size') !== null && + idx($props, 'tab_width') === null && + idx($props, 'indent_size') !== 'tab') { + return idx($props, 'indent_size'); + } + break; + } + + return idx($props, $key); + } + + /** + * Get the EditorConfig properties for the specified path. + * + * @param string + * @return map + */ + public function getProperties($path) { + $configs = $this->getEditorConfigs($path); + $matches = array(); + + foreach ($configs as $config_pair) { + list($path_prefix, $config) = $config_pair; + + foreach ($config as $glob => $options) { + if (!$glob) { + continue; + } + + if (strpos($glob, '/') === false) { + $glob = '**/'.$glob; + } else if (strncmp($glob, '/', 0)) { + $glob = substr($glob, 1); + } + + $glob = $path_prefix.'/'.$glob; + if (!phutil_fnmatch($glob, $path)) { + continue; + } + + foreach ($options as $option => $value) { + $option = strtolower($option); + + if (idx(self::$knownProperties, $option) === null) { + continue; + } + + // TODO: Maybe we should validate the data type of `$value` here. + + if (is_string($value)) { + $value = strtolower($value); + } + $matches[$option] = $value; + } + } + } + + return $matches; + } + + /** + * Returns the EditorConfig files which affect the specified path. + * + * Find and parse all `.editorconfig` files between the specified path and + * the root directory. The results are returned in the same order that they + * should be matched. + * + * return list> + */ + private function getEditorConfigs($path) { + $configs = array(); + $found_root = false; + $root = $this->root; + + do { + $path = dirname($path); + $file = $path.'/.editorconfig'; + + if (!Filesystem::pathExists($file)) { + continue; + } + + $contents = Filesystem::readFile($file); + $config = phutil_ini_decode($contents); + + if (idx($config, 'root')) { + $found_root = true; + } + unset($config['root']); + + array_unshift($configs, array($path, $config)); + + if ($found_root) { + break; + } + } while ($path != $root && Filesystem::isDescendant($path, $root)); + + return $configs; + } + +} 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,61 @@ + array( + 'indent_style' => 'tab', + 'indent_size' => 4, + 'charset' => 'utf-8', + 'trim_trailing_whitespace' => true, + 'insert_final_newline' => true, + ), + 'other-file' => array( + 'indent_style' => 'tab', + 'indent_size' => 4, + 'charset' => 'utf-8', + ), + 'file.txt' => array( + 'indent_style' => 'tab', + 'indent_size' => 4, + 'charset' => 'latin1', + 'trim_trailing_whitespace' => false, + 'insert_final_newline' => false, + ), + ); + $invalid_properties = array( + 'invalid', + ); + + foreach ($tests as $path => $config) { + foreach ($config as $key => $value) { + $this->assertEqual( + $value, + $parser->getProperty($this->getTestFile($path), $key)); + } + + $this->assertEqual( + $config, + $parser->getProperties($this->getTestFile($path))); + } + + foreach ($invalid_properties as $invalid_property) { + $caught = null; + try { + $parser->getProperty('', $invalid_property); + } catch (Exception $ex) { + $caught = $ex; + } + + $this->assertTrue($caught instanceof InvalidArgumentException); + } + } + + private function getTestFile($path) { + return dirname(__FILE__).'/editorconfig/'.$path; + } + +} diff --git a/src/parser/__tests__/editorconfig/.editorconfig b/src/parser/__tests__/editorconfig/.editorconfig new file mode 100644 --- /dev/null +++ b/src/parser/__tests__/editorconfig/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +indent_style = tab +indent_size = 4 +charset = utf-8 + +[*.txt] +charset = latin1 +trim_trailing_whitespace = false +insert_final_newline = false + +[file] +trim_trailing_whitespace = true +insert_final_newline = true