Issues (5)

src/SchemaParser.php (2 issues)

Labels
Severity
1
<?php
2
3
declare(strict_types=1);
4
5
namespace PHPTdGram\SchemaGenerator;
6
7
use PHPTdGram\SchemaGenerator\Model\ClassDefinition;
8
use Symfony\Component\Console\Output\OutputInterface;
9
10
/**
11
 * @author  Aurimas Niekis <[email protected]>
12
 */
13
class SchemaParser
14
{
15
    public const TL_API_SOURCE_URL = 'https://raw.githubusercontent.com/tdlib/td/v1.6.0/td/generate/scheme/td_api.tl';
16
17
    private OutputInterface $output;
18
19
    private string          $schemaFile;
20
    private string          $rawSchema;
21
    private string          $currentLine;
22
    private int             $currentLineNr;
23
    private array           $documentation;
24
    private array           $classes;
25
26
    public function __construct(OutputInterface $output, string $schemaFile = null)
27
    {
28
        $this->output        = $output;
29
        $this->schemaFile    = $schemaFile ?? static::TL_API_SOURCE_URL;
30
        $this->rawSchema     = file_get_contents($this->schemaFile);
31
        $this->documentation = [];
32
        $this->classes       = [];
33
    }
34
35
    /**
36
     * @return ClassDefinition[]
37
     */
38
    public function parse(): array
39
    {
40
        $lines = explode(PHP_EOL, $this->rawSchema);
41
42
        $description          = '';
43
        $currentClassName     = '';
44
        $currentClass         = new ClassDefinition();
45
        $isFunction           = false;
46
        $needClassDescription = false;
47
        $this->currentLineNr  = 0;
48
49
        foreach ($lines as $line) {
50
            $this->currentLine = $line;
51
            $this->currentLineNr++;
52
53
            if ('---types---' === $line) {
54
                $isFunction = false;
55
            } elseif ('---functions---' === $line) {
56
                $isFunction           = true;
57
                $currentClassName     = '';
58
                $currentClass         = new ClassDefinition();
59
                $needClassDescription = false;
60
            } elseif (($line[0] ?? '') === '/') {
61
                if (($line[1] ?? '') !== '/') {
62
                    $this->printError('Wrong comment');
63
64
                    continue;
65
                }
66
67
                if (($line[2] ?? '') === '@' || ($line[2] ?? '') === '-') {
68
                    $description .= trim(substr($line, 2 + intval('-' === $line[2]))) . ' ';
69
                } else {
70
                    $this->printError('Unexpected comment');
71
                }
72
            } elseif (strpos($line, '? =') || strpos($line, ' = Vector t;') || 'boolFalse = Bool;' === $line ||
73
                'boolTrue = Bool;' === $line || 'bytes = Bytes;' === $line || 'int32 = Int32;' === $line ||
74
                'int53 = Int53;' === $line || 'int64 = Int64;' === $line) {
75
                $this->printDebug('skip built-in types');
76
77
                continue;
78
            } else {
79
                $description = trim($description);
80
81
                if ('' === $description) {
82
//                    $this->printError('Empty description', ['description' => $description]);
83
                }
84
85
                if (($description[0] ?? '') !== '@') {
86
//                    $this->printError('Wrong description begin', ['description' => $description]);
87
                }
88
89
                $docs = explode('@', $description);
90
                array_shift($docs);
91
92
                $info = [];
93
                foreach ($docs as $doc) {
94
                    [$key, $value] = explode(' ', $doc, 2);
95
                    $value         = trim($value);
96
97
                    if ($needClassDescription) {
98
                        if ('description' === $key) {
99
                            $needClassDescription = false;
100
101
                            $currentClass->classDocs   = $value;
102
                            $currentClass->parentClass = 'Object';
103
                            $currentClass->typeName    = $currentClass->className;
104
105
                            $this->classes[$value] = $currentClass;
106
                            $currentClass          = new ClassDefinition();
107
                            continue;
108
                        } else {
109
                            $this->printError('Expected abstract class description', ['description' => $description]);
110
                        }
111
                    }
112
113
                    if ('class' === $key) {
114
                        $currentClassName        = $this->getClassName($value);
115
                        $currentClass->className = $currentClassName;
116
117
                        $needClassDescription = true;
118
119
                        if ($isFunction) {
120
                            $this->printError('Unexpected class definition');
121
                        }
122
                    } else {
123
                        if (isset($info[$key])) {
124
//                            $this->printError("Duplicate info about `$key`");
125
                        }
126
                        $info[$key] = trim($value);
127
                    }
128
                }
129
130
                if (1 !== substr_count($line, '=')) {
131
//                    $this->printError("Wrong '=' count");
132
                    continue;
133
                }
134
135
                [$fields, $type] = explode('=', $line);
136
                $type            = $this->getClassName($type);
137
                $fields          = explode(' ', trim($fields));
138
                $typeName        = array_shift($fields);
139
                $className       = $this->getClassName($typeName);
140
141
                if ($type !== $currentClassName) {
142
                    $currentClassName     = '';
143
                    $currentClass         = new ClassDefinition();
144
                    $needClassDescription = false;
145
                }
146
147
                if (!$isFunction) {
148
                    $typeLower      = strtolower($type);
149
                    $classNameLower = strtolower($className);
150
151
                    if (empty($currentClassName) === ($typeLower !== $classNameLower)) {
152
                        $this->printError('Wrong constructor name');
153
                    }
154
155
                    if (0 !== strpos($classNameLower, $typeLower)) {
156
                        // $this->printError('Wrong constructor name');
157
                    }
158
                }
159
160
                $knownFields = [];
161
                foreach ($fields as $field) {
162
                    [$fieldName, $fieldType] = explode(':', $field);
163
164
                    if (isset($info['param_' . $fieldName])) {
165
                        $knownFields['param_' . $fieldName] = $fieldType;
166
167
                        continue;
168
                    }
169
170
                    if (isset($info[$fieldName])) {
171
                        $knownFields[$fieldName] = $fieldType;
172
173
                        continue;
174
                    }
175
176
                    $this->printError("Have no info about field `$fieldName`");
177
                }
178
179
                foreach ($info as $name => $value) {
180
                    if (!$value) {
181
                        $this->printError("info[$name] for $className is empty");
182
                    } elseif (($value[0] < 'A' || $value[0] > 'Z') && ($value[0] < '0' || $value[0] > '9')) {
183
                        $this->printError("info[$name] for $className doesn't begins with capital letter");
184
                    }
185
                }
186
187
                foreach (array_diff_key($info, $knownFields) as $fieldName => $fieldInfo) {
188
                    if ('description' !== $fieldName) {
189
                        $this->printError("Have info about unexisted field `$fieldName`");
190
                    }
191
                }
192
193
                if (!isset($info['description'])) {
194
                    $this->printError("Have no description for class `$className`");
195
                }
196
197
                $baseClassName    = $currentClassName ?: $this->getBaseClassName($isFunction);
198
                $classDescription = $info['description'];
199
200
                if ($isFunction) {
201
                    $currentClass->returnType = $this->getTypeName($type);
202
                }
203
204
                $currentClass->className   = $className;
205
                $currentClass->parentClass = $baseClassName;
206
                $currentClass->classDocs   = $classDescription;
207
                $currentClass->typeName    = $typeName;
208
209
                foreach ($knownFields as $name => $fieldType) {
210
                    $mayBeNull     = false !== stripos($info[$name], 'may be null');
211
                    $fieldName     = $this->getFieldName($name, $className);
212
                    $fieldTypeName = $this->getTypeName($fieldType);
213
214
                    $rawName = $name;
215
                    if ('param_' === substr($rawName, 0, 6)) {
216
                        $rawName = substr($rawName, 6);
217
                    }
218
219
                    $field            = $currentClass->getField($name);
220
                    $field->rawName   = $rawName;
221
                    $field->name      = $fieldName;
222
                    $field->type      = $fieldTypeName;
223
                    $field->doc       = $info[$name];
224
                    $field->mayBeNull = $mayBeNull;
225
                }
226
227
                $this->classes[$typeName] = $currentClass;
228
                $currentClass             = new ClassDefinition();
229
                $description              = '';
230
            }
231
        }
232
233
        return $this->classes;
234
    }
235
236
    private function printError(string $msg, array $args = []): void
237
    {
238
        $this->output->writeln(
239
            '<error>' . $msg . '</error>',
240
        );
241
242
        dump(['line' => $this->currentLine, 'line_nr' => $this->currentLineNr], $args);
0 ignored issues
show
The function dump was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

242
        /** @scrutinizer ignore-call */ 
243
        dump(['line' => $this->currentLine, 'line_nr' => $this->currentLineNr], $args);
Loading history...
243
    }
244
245
    private function printDebug(string $msg, array $args = []): void
246
    {
247
        if ($this->output->isDebug()) {
248
            $this->output->writeln('<debug>' . $msg . '</debug>');
249
250
            dump(['line' => $this->currentLine, 'line_nr' => $this->currentLineNr], $args);
0 ignored issues
show
The function dump was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

250
            /** @scrutinizer ignore-call */ 
251
            dump(['line' => $this->currentLine, 'line_nr' => $this->currentLineNr], $args);
Loading history...
251
        }
252
    }
253
254
    protected function getClassName($name): string
255
    {
256
        return implode(array_map('ucfirst', explode('.', trim($name, "\r\n ;"))));
257
    }
258
259
    protected function getBaseClassName($isFunction): string
260
    {
261
        return $isFunction ? 'Function' : 'Object';
262
    }
263
264
    protected function getTypeName($type): string
265
    {
266
        switch ($type) {
267
            case 'Bool':
268
                return 'bool';
269
            case 'int32':
270
            case 'int53':
271
                return 'int';
272
            case 'double':
273
                return 'float';
274
            case 'string':
275
            case 'bytes':
276
            case 'int64':
277
                return 'string';
278
            case 'bool':
279
            case 'int':
280
            case 'long':
281
            case 'Int':
282
            case 'Long':
283
            case 'Int32':
284
            case 'Int53':
285
            case 'Int64':
286
            case 'Double':
287
            case 'String':
288
            case 'Bytes':
289
                $this->printError("Wrong type $type");
290
291
                return '';
292
            default:
293
                if ('vector' === substr($type, 0, 6)) {
294
                    if ('<' !== $type[6] || '>' !== $type[strlen($type) - 1]) {
295
                        $this->printError("Wrong vector subtype in $type");
296
297
                        return '';
298
                    }
299
300
                    return $this->getTypeName(substr($type, 7, -1)) . '[]';
301
                }
302
303
                if (preg_match('/[^A-Za-z0-9.]/', $type)) {
304
                    $this->printError("Wrong type $type");
305
306
                    return '';
307
                }
308
309
                return $this->getClassName($type);
310
        }
311
    }
312
313
    protected function getFieldName($name, $className): string
314
    {
315
        if ('param_' === substr($name, 0, 6)) {
316
            $name = substr($name, 6);
317
        }
318
319
        return preg_replace_callback(
320
            '/_([A-Za-z])/',
321
            fn ($matches) => strtoupper($matches[1]),
322
            trim($name)
323
        );
324
    }
325
}
326