PageRenderTime 58ms CodeModel.GetById 29ms RepoModel.GetById 1ms app.codeStats 0ms

/src/infrastructure/internationalization/management/PhabricatorInternationalizationManagementExtractWorkflow.php

http://github.com/facebook/phabricator
PHP | 354 lines | 289 code | 65 blank | 0 comment | 27 complexity | 6242f5245bebdc8d18040af252ca7dd9 MD5 | raw file
Possible License(s): JSON, MPL-2.0-no-copyleft-exception, Apache-2.0, BSD-3-Clause, LGPL-2.0, MIT, LGPL-2.1, LGPL-3.0
  1. <?php
  2. final class PhabricatorInternationalizationManagementExtractWorkflow
  3. extends PhabricatorInternationalizationManagementWorkflow {
  4. const CACHE_VERSION = 1;
  5. protected function didConstruct() {
  6. $this
  7. ->setName('extract')
  8. ->setExamples(
  9. '**extract** [__options__] __library__')
  10. ->setSynopsis(pht('Extract translatable strings.'))
  11. ->setArguments(
  12. array(
  13. array(
  14. 'name' => 'paths',
  15. 'wildcard' => true,
  16. ),
  17. array(
  18. 'name' => 'clean',
  19. 'help' => pht('Drop caches before extracting strings. Slow!'),
  20. ),
  21. ));
  22. }
  23. public function execute(PhutilArgumentParser $args) {
  24. $console = PhutilConsole::getConsole();
  25. $paths = $args->getArg('paths');
  26. if (!$paths) {
  27. $paths = array(getcwd());
  28. }
  29. $targets = array();
  30. foreach ($paths as $path) {
  31. $root = Filesystem::resolvePath($path);
  32. if (!Filesystem::pathExists($root) || !is_dir($root)) {
  33. throw new PhutilArgumentUsageException(
  34. pht(
  35. 'Path "%s" does not exist, or is not a directory.',
  36. $path));
  37. }
  38. $libraries = id(new FileFinder($path))
  39. ->withPath('*/__phutil_library_init__.php')
  40. ->find();
  41. if (!$libraries) {
  42. throw new PhutilArgumentUsageException(
  43. pht(
  44. 'Path "%s" contains no libphutil libraries.',
  45. $path));
  46. }
  47. foreach ($libraries as $library) {
  48. $targets[] = Filesystem::resolvePath(dirname($path.'/'.$library)).'/';
  49. }
  50. }
  51. $targets = array_unique($targets);
  52. foreach ($targets as $library) {
  53. echo tsprintf(
  54. "**<bg:blue> %s </bg>** %s\n",
  55. pht('EXTRACT'),
  56. pht(
  57. 'Extracting "%s"...',
  58. Filesystem::readablePath($library)));
  59. $this->extractLibrary($library);
  60. }
  61. return 0;
  62. }
  63. private function extractLibrary($root) {
  64. $files = $this->loadLibraryFiles($root);
  65. $cache = $this->readCache($root);
  66. $modified = $this->getModifiedFiles($files, $cache);
  67. $cache['files'] = $files;
  68. if ($modified) {
  69. echo tsprintf(
  70. "**<bg:blue> %s </bg>** %s\n",
  71. pht('MODIFIED'),
  72. pht(
  73. 'Found %s modified file(s) (of %s total).',
  74. phutil_count($modified),
  75. phutil_count($files)));
  76. $old_strings = idx($cache, 'strings');
  77. $old_strings = array_select_keys($old_strings, $files);
  78. $new_strings = $this->extractFiles($root, $modified);
  79. $all_strings = $new_strings + $old_strings;
  80. $cache['strings'] = $all_strings;
  81. $this->writeStrings($root, $all_strings);
  82. } else {
  83. echo tsprintf(
  84. "**<bg:blue> %s </bg>** %s\n",
  85. pht('NOT MODIFIED'),
  86. pht('Strings for this library are already up to date.'));
  87. }
  88. $cache = id(new PhutilJSON())->encodeFormatted($cache);
  89. $this->writeCache($root, 'i18n_files.json', $cache);
  90. }
  91. private function getModifiedFiles(array $files, array $cache) {
  92. $known = idx($cache, 'files', array());
  93. $known = array_fuse($known);
  94. $modified = array();
  95. foreach ($files as $file => $hash) {
  96. if (isset($known[$hash])) {
  97. continue;
  98. }
  99. $modified[$file] = $hash;
  100. }
  101. return $modified;
  102. }
  103. private function extractFiles($root_path, array $files) {
  104. $hashes = array();
  105. $futures = array();
  106. foreach ($files as $file => $hash) {
  107. $full_path = $root_path.DIRECTORY_SEPARATOR.$file;
  108. $data = Filesystem::readFile($full_path);
  109. $futures[$full_path] = PhutilXHPASTBinary::getParserFuture($data);
  110. $hashes[$full_path] = $hash;
  111. }
  112. $bar = id(new PhutilConsoleProgressBar())
  113. ->setTotal(count($futures));
  114. $messages = array();
  115. $results = array();
  116. $futures = id(new FutureIterator($futures))
  117. ->limit(8);
  118. foreach ($futures as $full_path => $future) {
  119. $bar->update(1);
  120. $hash = $hashes[$full_path];
  121. try {
  122. $tree = XHPASTTree::newFromDataAndResolvedExecFuture(
  123. Filesystem::readFile($full_path),
  124. $future->resolve());
  125. } catch (Exception $ex) {
  126. $messages[] = pht(
  127. 'WARNING: Failed to extract strings from file "%s": %s',
  128. $full_path,
  129. $ex->getMessage());
  130. continue;
  131. }
  132. $root = $tree->getRootNode();
  133. $calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
  134. foreach ($calls as $call) {
  135. $name = $call->getChildByIndex(0)->getConcreteString();
  136. if ($name != 'pht') {
  137. continue;
  138. }
  139. $params = $call->getChildByIndex(1, 'n_CALL_PARAMETER_LIST');
  140. $string_node = $params->getChildByIndex(0);
  141. $string_line = $string_node->getLineNumber();
  142. try {
  143. $string_value = $string_node->evalStatic();
  144. $args = $params->getChildren();
  145. $args = array_slice($args, 1);
  146. $types = array();
  147. foreach ($args as $child) {
  148. $type = null;
  149. switch ($child->getTypeName()) {
  150. case 'n_FUNCTION_CALL':
  151. $call = $child->getChildByIndex(0);
  152. if ($call->getTypeName() == 'n_SYMBOL_NAME') {
  153. switch ($call->getConcreteString()) {
  154. case 'phutil_count':
  155. $type = 'number';
  156. break;
  157. case 'phutil_person':
  158. $type = 'person';
  159. break;
  160. }
  161. }
  162. break;
  163. case 'n_NEW':
  164. $class = $child->getChildByIndex(0);
  165. if ($class->getTypeName() == 'n_CLASS_NAME') {
  166. switch ($class->getConcreteString()) {
  167. case 'PhutilNumber':
  168. $type = 'number';
  169. break;
  170. }
  171. }
  172. break;
  173. default:
  174. break;
  175. }
  176. $types[] = $type;
  177. }
  178. $results[$hash][] = array(
  179. 'string' => $string_value,
  180. 'file' => Filesystem::readablePath($full_path, $root_path),
  181. 'line' => $string_line,
  182. 'types' => $types,
  183. );
  184. } catch (Exception $ex) {
  185. $messages[] = pht(
  186. 'WARNING: Failed to evaluate pht() call on line %d in "%s": %s',
  187. $call->getLineNumber(),
  188. $full_path,
  189. $ex->getMessage());
  190. }
  191. }
  192. $tree->dispose();
  193. }
  194. $bar->done();
  195. foreach ($messages as $message) {
  196. echo tsprintf(
  197. "**<bg:yellow> %s </bg>** %s\n",
  198. pht('WARNING'),
  199. $message);
  200. }
  201. return $results;
  202. }
  203. private function writeStrings($root, array $strings) {
  204. $map = array();
  205. foreach ($strings as $hash => $string_list) {
  206. foreach ($string_list as $string_info) {
  207. $string = $string_info['string'];
  208. $map[$string]['uses'][] = array(
  209. 'file' => $string_info['file'],
  210. 'line' => $string_info['line'],
  211. );
  212. if (!isset($map[$string]['types'])) {
  213. $map[$string]['types'] = $string_info['types'];
  214. } else if ($map[$string]['types'] !== $string_info['types']) {
  215. echo tsprintf(
  216. "**<bg:yellow> %s </bg>** %s\n",
  217. pht('WARNING'),
  218. pht(
  219. 'Inferred types for string "%s" vary across callsites.',
  220. $string_info['string']));
  221. }
  222. }
  223. }
  224. ksort($map);
  225. $json = id(new PhutilJSON())->encodeFormatted($map);
  226. $this->writeCache($root, 'i18n_strings.json', $json);
  227. }
  228. private function loadLibraryFiles($root) {
  229. $files = id(new FileFinder($root))
  230. ->withType('f')
  231. ->withSuffix('php')
  232. ->excludePath('*/.*')
  233. ->setGenerateChecksums(true)
  234. ->find();
  235. $map = array();
  236. foreach ($files as $file => $hash) {
  237. $file = Filesystem::readablePath($file, $root);
  238. $file = ltrim($file, '/');
  239. if (dirname($file) == '.') {
  240. continue;
  241. }
  242. if (dirname($file) == 'extensions') {
  243. continue;
  244. }
  245. $map[$file] = md5($hash.$file);
  246. }
  247. return $map;
  248. }
  249. private function readCache($root) {
  250. $path = $this->getCachePath($root, 'i18n_files.json');
  251. $default = array(
  252. 'version' => self::CACHE_VERSION,
  253. 'files' => array(),
  254. 'strings' => array(),
  255. );
  256. if ($this->getArgv()->getArg('clean')) {
  257. return $default;
  258. }
  259. if (!Filesystem::pathExists($path)) {
  260. return $default;
  261. }
  262. try {
  263. $data = Filesystem::readFile($path);
  264. } catch (Exception $ex) {
  265. return $default;
  266. }
  267. try {
  268. $cache = phutil_json_decode($data);
  269. } catch (PhutilJSONParserException $e) {
  270. return $default;
  271. }
  272. $version = idx($cache, 'version');
  273. if ($version !== self::CACHE_VERSION) {
  274. return $default;
  275. }
  276. return $cache;
  277. }
  278. private function writeCache($root, $file, $data) {
  279. $path = $this->getCachePath($root, $file);
  280. $cache_dir = dirname($path);
  281. if (!Filesystem::pathExists($cache_dir)) {
  282. Filesystem::createDirectory($cache_dir, 0755, true);
  283. }
  284. Filesystem::writeFile($path, $data);
  285. }
  286. private function getCachePath($root, $to_file) {
  287. return $root.'/.cache/'.$to_file;
  288. }
  289. }