Page MenuHomePhabricator

D9678.diff
No OneTemporary

D9678.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',
'PhutilEmptyAuthAdapter' => 'auth/PhutilEmptyAuthAdapter.php',
@@ -594,6 +596,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,180 @@
+<?php
+
+/**
+ * Parser for [[http://editorconfig.org/ | EditorConfig]] files.
+ */
+final class PhutilEditorConfig {
+
+ /**
+ * Valid properties.
+ *
+ * See http://editorconfig.org/#file-format-details.
+ */
+ private static $knownProperties = array(
+ 'charset' => array('latin1', 'utf-8', 'utf-8-bom', 'utf-16be', 'utf-16le'),
+ 'end_of_line' => array('lf', 'cr', 'crlf'),
+ 'indent_size' => 'int|string',
+ '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)) {
+ 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_width') === null) {
+ return idx($props, 'tab_width');
+ }
+ break;
+
+ case 'tab_width':
+ if (idx($props, 'tab_width') === null &&
+ idx($props, 'indent_size') !== null &&
+ idx($props, 'indent_size') !== 'tab') {
+ return idx($props, 'indent_size');
+ }
+ break;
+ }
+
+ return idx($props, $key);
+ }
+
+ /**
+ * Get the EditorConfig properties for the specified path.
+ *
+ * Returns a map containing all of the EditorConfig properties which apply
+ * to the specified path. The following rules are applied when processing
+ * EditorConfig files:
+ *
+ * - If a glob does not contain `/`, it can match a path in any subdirectory.
+ * - If the first character of a glob is `/`, it will only match files in the
+ * same directory as the `.editorconfig` file.
+ * - Properties and values are case-insensitive.
+ * - Unknown properties will be silently ignored.
+ * - Values are not validated against the specification (this may change in
+ * the future).
+ * - Invalid glob patterns will be silently ignored.
+ *
+ * @param string
+ * @return map<string, wild>
+ */
+ public function getProperties($path) {
+ $configs = $this->getEditorConfigs($path);
+ $matches = array();
+
+ foreach ($configs as $config) {
+ list($path_prefix, $editorconfig) = $config;
+
+ foreach ($editorconfig as $glob => $properties) {
+ if (!$glob) {
+ continue;
+ }
+
+ if (strpos($glob, '/') === false) {
+ $glob = '**/'.$glob;
+ } else if (strncmp($glob, '/', 0)) {
+ $glob = substr($glob, 1);
+ }
+
+ $glob = $path_prefix.'/'.$glob;
+ try {
+ if (!phutil_fnmatch($glob, $path)) {
+ continue;
+ }
+ } catch (Exception $ex) {
+ // Invalid glob pattern... ignore it.
+ continue;
+ }
+
+ foreach ($properties as $property => $value) {
+ $property = strtolower($property);
+
+ if (!idx(self::$knownProperties, $property)) {
+ // Unknown property... ignore it.
+ continue;
+ }
+
+ if (is_string($value)) {
+ $value = strtolower($value);
+ }
+ if ($value === '') {
+ $value = null;
+ }
+ $matches[$property] = $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<pair<string, map>>
+ */
+ 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') === true) {
+ $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,106 @@
+<?php
+
+final class PhutilEditorConfigTestCase extends PhutilTestCase {
+
+ public function testGetProperty() {
+ $parser = new PhutilEditorConfig($this->getTestFile());
+
+ $tests = array(
+ 'default' => array(
+ array(
+ 'indent_style' => 'space',
+ 'indent_size' => 2,
+ 'charset' => 'utf-8',
+ 'trim_trailing_whitespace' => true,
+ 'insert_final_newline' => true,
+ ),
+ array(),
+ ),
+ 'file' => array(
+ array(
+ 'indent_style' => 'space',
+ 'indent_size' => 3,
+ 'charset' => 'utf-8',
+ 'trim_trailing_whitespace' => true,
+ 'insert_final_newline' => true,
+ ),
+ array(),
+ ),
+ 'file.txt' => array(
+ array(
+ 'indent_style' => 'space',
+ 'indent_size' => 3,
+ 'charset' => 'latin1',
+ 'trim_trailing_whitespace' => true,
+ 'insert_final_newline' => true,
+ ),
+ array(),
+ ),
+ 'externals/README' => array(
+ array(
+ 'indent_style' => null,
+ 'indent_size' => null,
+ 'charset' => 'utf-8',
+ 'trim_trailing_whitespace' => false,
+ 'insert_final_newline' => false,
+ ),
+ array(),
+ ),
+ 'subdir/file' => array(
+ array(
+ 'indent_style' => 'tab',
+ 'indent_size' => 3,
+ 'charset' => 'utf-8-bom',
+ 'trim_trailing_whitespace' => true,
+ 'insert_final_newline' => true,
+ ),
+ array(),
+ ),
+ 'empty/file' => array(
+ array(),
+ array(
+ 'indent_style' => null,
+ 'indent_size' => null,
+ 'charset' => null,
+ 'trim_trailing_whitespace' => null,
+ 'insert_final_newline' => null,
+ ),
+ ),
+ );
+
+ foreach ($tests as $path => $expected) {
+ list($properties, $property) = $expected;
+ $property = array_merge($properties, $property);
+
+ $this->assertEqual(
+ $properties,
+ $parser->getProperties($this->getTestFile($path)));
+
+ foreach ($property as $key => $value) {
+ $this->assertEqual(
+ $value,
+ $parser->getProperty($this->getTestFile($path), $key));
+ }
+ }
+
+ $invalid_properties = array(
+ 'invalid',
+ );
+
+ 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 = null) {
+ 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,23 @@
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[file*]
+indent_size = 3
+
+[*.txt]
+charset = latin1
+
+[{invalid_glob]
+indent_size = 1
+
+[externals/**]
+indent_style =
+indent_size =
+trim_trailing_whitespace = false
+insert_final_newline = false
diff --git a/src/parser/__tests__/editorconfig/empty/.editorconfig b/src/parser/__tests__/editorconfig/empty/.editorconfig
new file mode 100644
--- /dev/null
+++ b/src/parser/__tests__/editorconfig/empty/.editorconfig
@@ -0,0 +1 @@
+root = true
diff --git a/src/parser/__tests__/editorconfig/externals/.editorconfig b/src/parser/__tests__/editorconfig/externals/.editorconfig
new file mode 100644
diff --git a/src/parser/__tests__/editorconfig/subdir/.editorconfig b/src/parser/__tests__/editorconfig/subdir/.editorconfig
new file mode 100644
--- /dev/null
+++ b/src/parser/__tests__/editorconfig/subdir/.editorconfig
@@ -0,0 +1,3 @@
+[*]
+indent_style = tab
+charset = utf-8-bom

File Metadata

Mime Type
text/plain
Expires
Fri, Dec 20, 4:36 AM (20 h, 33 m)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6909523
Default Alt Text
D9678.diff (10 KB)

Event Timeline