current_line)."\"\n"); } final protected function addDocumentation($code, $doc) { if (isset($this->documentation[$code])) { $this->printError("Duplicate documentation for \"$code\""); } $this->documentation[$code] = $doc; // $this->printError($code); } final protected function addLineReplacement($line, $new_line) { if (isset($this->line_replacement[$line])) { $this->printError("Duplicate line replacement for \"$line\""); } $this->line_replacement[$line] = $new_line; } final protected function addDot($str) { if (!$str) { return ''; } $len = strlen($str); if ($str[$len - 1] === '.') { return $str; } if ($str[$len - 1] === ')') { // trying to place dot inside the brackets $bracket_count = 1; for ($pos = $len - 2; $pos >= 0; $pos--) { if ($str[$pos] === ')') { $bracket_count++; } if ($str[$pos] === '(') { $bracket_count--; if ($bracket_count === 0) { break; } } } if ($bracket_count === 0) { if (ord('A') <= ord($str[$pos + 1]) && ord($str[$pos + 1]) <= ord('Z')) { return substr($str, 0, -1).'.)'; } } else { $this->printError("Unmatched bracket"); } } return $str.'.'; } abstract protected function escapeDocumentation($doc); abstract protected function getFieldName($name, $class_name); abstract protected function getClassName($name); abstract protected function getTypeName($type); abstract protected function getBaseClassName($is_function); abstract protected function needRemoveLine($line); abstract protected function needSkipLine($line); abstract protected function isHeaderLine($line); abstract protected function extractClassName($line); abstract protected function fixLine($line); abstract protected function addGlobalDocumentation(); abstract protected function addAbstractClassDocumentation($class_name, $value); abstract protected function getFunctionReturnTypeDescription($return_type, $for_constructor); abstract protected function addClassDocumentation($class_name, $base_class_name, $return_type, $description); abstract protected function addFieldDocumentation($class_name, $field_name, $type_name, $field_info, $may_be_null); abstract protected function addDefaultConstructorDocumentation($class_name, $class_description); abstract protected function addFullConstructorDocumentation($class_name, $class_description, $known_fields, $info); public function generate($tl_scheme_file, $source_file) { $lines = array_filter(array_map('trim', file($tl_scheme_file))); $description = ''; $description_line_count = 0; $current_class = ''; $is_function = false; $need_class_description = false; $this->addGlobalDocumentation(); foreach ($lines as $line) { $this->current_line = $line; if ($line === '---types---') { $is_function = false; } elseif ($line === '---functions---') { $is_function = true; $current_class = ''; $need_class_description = false; } elseif ($line[0] === '/') { if ($line[1] !== '/') { $this->printError('Wrong comment'); continue; } if ($line[2] === '@') { if (substr($line, 2, 7) !== '@class ') { $description_line_count++; } $description .= trim(substr($line, 2)).' '; } elseif ($line[2] === '-') { $description .= trim(substr($line, 3)).' '; } else { $this->printError('Unexpected comment'); } } elseif (strpos($line, '? =') || strpos($line, ' = Vector t;') || $line === 'boolFalse = Bool;' || $line === 'boolTrue = Bool;' || $line === 'bytes = Bytes;' || $line === 'int32 = Int32;' || $line === 'int53 = Int53;'|| $line === 'int64 = Int64;') { // skip built-in types continue; } else { $description = trim($description); if ($description[0] !== '@') { $this->printError('Wrong description begin'); } if (preg_match('/[^ ]@/', $description)) { $this->printError("Wrong documentation '@' usage: $description"); } $docs = explode('@', $description); array_shift($docs); $info = array(); foreach ($docs as $doc) { list($key, $value) = explode(' ', $doc, 2); $value = trim($value); if ($need_class_description) { if ($key === 'description') { $need_class_description = false; $value = $this->escapeDocumentation($this->addDot($value)); $this->addAbstractClassDocumentation($current_class, $value); continue; } else { $this->printError('Expected abstract class description'); } } if ($key === 'class') { $current_class = $this->getClassName($value); $need_class_description = true; if ($is_function) { $this->printError('Unexpected class definition'); } } else { if (isset($info[$key])) { $this->printError("Duplicate info about `$key`"); } $info[$key] = trim($value); } } if (substr_count($line, '=') !== 1) { $this->printError("Wrong '=' count"); continue; } list($fields, $type) = explode('=', $line); $type = $this->getClassName($type); $fields = explode(' ', trim($fields)); $class_name = $this->getClassName(array_shift($fields)); if ($type !== $current_class) { $current_class = ''; $need_class_description = false; } if (!$is_function) { $type_lower = strtolower($type); $class_name_lower = strtolower($class_name); if (empty($current_class) === ($type_lower !== $class_name_lower)) { $this->printError('Wrong constructor name'); } if (strpos($class_name_lower, $type_lower) !== 0) { // $this->printError('Wrong constructor name'); } } $known_fields = array(); foreach ($fields as $field) { list ($field_name, $field_type) = explode(':', $field); if (isset($info['param_'.$field_name])) { $known_fields['param_'.$field_name] = $field_type; continue; } if (isset($info[$field_name])) { $known_fields[$field_name] = $field_type; continue; } $this->printError("Have no documentation for field `$field_name`"); } foreach ($info as $name => $value) { if (!$value) { $this->printError("Documentation for field $name of $class_name is empty"); } elseif (($value[0] < 'A' || $value[0] > 'Z') && ($value[0] < '0' || $value[0] > '9')) { $this->printError("Documentation for field $name of $class_name doesn't begin with a capital letter"); } } foreach ($info as &$v) { $v = $this->escapeDocumentation($this->addDot($v)); } $description = $info['description']; unset($info['description']); if (!$description) { $this->printError("Have no description for class `$class_name`"); } foreach (array_diff_key($info, $known_fields) as $field_name => $field_info) { $this->printError("Have info about nonexistent field `$field_name`"); } if (array_keys($info) !== array_keys($known_fields)) { $this->printError("Have wrong documentation for class `$class_name`"); } else if ($description_line_count !== 1 && $description_line_count !== count($known_fields) + 1) { $this->printError("Documentation for fields of class `$class_name` must be split to different lines"); } $base_class_name = $current_class ?: $this->getBaseClassName($is_function); $class_description = $description; $return_type = ""; if ($is_function) { $return_type = $this->getTypeName($type); $class_description .= $this->getFunctionReturnTypeDescription($return_type, false); } $this->addClassDocumentation($class_name, $base_class_name, $return_type, $class_description); foreach ($known_fields as $name => $field_type) { $may_be_null = stripos($info[$name], 'may be null') !== false; $field_name = $this->getFieldName($name, $class_name); $field_type_name = $this->getTypeName($field_type); $this->addFieldDocumentation($class_name, $field_name, $field_type_name, $info[$name], $may_be_null); } if ($is_function) { $default_constructor_prefix = 'Default constructor for a function, which '; $full_constructor_prefix = 'Creates a function, which '; $class_description = lcfirst($description); $class_description .= $this->getFunctionReturnTypeDescription($this->getTypeName($type), true); } else { $default_constructor_prefix = ''; $full_constructor_prefix = ''; } $this->addDefaultConstructorDocumentation($class_name, $default_constructor_prefix.$class_description); if ($known_fields) { $this->addFullConstructorDocumentation($class_name, $full_constructor_prefix.$class_description, $known_fields, $info); } $description = ''; $description_line_count = 0; } } $lines = file($source_file); $result = ''; $current_class = ''; $current_headers = ''; foreach ($lines as $line) { $this->current_line = $line; if ($this->needRemoveLine($line)) { continue; } if ($this->needSkipLine($line)) { $result .= $current_headers.$line; $current_headers = ''; continue; } if ($this->isHeaderLine($line)) { $current_headers .= $line; continue; } $current_class = $this->extractClassName($line) ?: $current_class; $fixed_line = rtrim($this->fixLine($line)); $doc = ''; if (isset($this->documentation[$fixed_line])) { $doc = $this->documentation[$fixed_line]; // unset($this->documentation[$fixed_line]); } elseif (isset($this->documentation[$current_class.$fixed_line])) { $doc = $this->documentation[$current_class.$fixed_line]; // unset($this->documentation[$current_class.$fixed_line]); } else { $this->printError('Have no docs for "'.$fixed_line.'"'); } if ($doc) { $result .= $doc."\n"; } if (isset($this->line_replacement[$fixed_line])) { $line = $this->line_replacement[$fixed_line]; } elseif (isset($this->line_replacement[$current_class.$fixed_line])) { $line = $this->line_replacement[$current_class.$fixed_line]; } $result .= $current_headers.$line; $current_headers = ''; } if (file_get_contents($source_file) !== $result) { file_put_contents($source_file, $result); } if (count($this->documentation)) { // $this->printError('Have unused docs '.print_r(array_keys($this->documentation), true)); } } }