ether-code 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2404 @@
1
+ const { EtherParserBase, TokenType } = require('./ether-parser-base')
2
+
3
+ class EtherParserPHP extends EtherParserBase {
4
+ constructor(options = {}) {
5
+ super(options)
6
+ this.initPHPTranslations()
7
+ this.loadI18n(['i18n-php.json'])
8
+ }
9
+
10
+ initPHPTranslations() {
11
+ this.phpTranslations = {
12
+ 'variable': '$',
13
+ 'constante': 'const',
14
+ 'definir': 'define',
15
+ 'fonction': 'function',
16
+ 'retourner': 'return',
17
+ 'si': 'if',
18
+ 'sinon': 'else',
19
+ 'sinon si': 'elseif',
20
+ 'pour': 'for',
21
+ 'pour chaque': 'foreach',
22
+ 'tant que': 'while',
23
+ 'faire': 'do',
24
+ 'selon': 'switch',
25
+ 'cas': 'case',
26
+ 'defaut': 'default',
27
+ 'sortir': 'break',
28
+ 'continuer': 'continue',
29
+ 'essayer': 'try',
30
+ 'attraper': 'catch',
31
+ 'finalement': 'finally',
32
+ 'lancer': 'throw',
33
+ 'nouveau': 'new',
34
+ 'nouvelle': 'new',
35
+ 'classe': 'class',
36
+ 'interface': 'interface',
37
+ 'trait': 'trait',
38
+ 'etendre': 'extends',
39
+ 'etend': 'extends',
40
+ 'implementer': 'implements',
41
+ 'implemente': 'implements',
42
+ 'abstrait': 'abstract',
43
+ 'abstraite': 'abstract',
44
+ 'final': 'final',
45
+ 'finale': 'final',
46
+ 'public': 'public',
47
+ 'publique': 'public',
48
+ 'prive': 'private',
49
+ 'privee': 'private',
50
+ 'protege': 'protected',
51
+ 'protegee': 'protected',
52
+ 'statique': 'static',
53
+ 'readonly': 'readonly',
54
+ 'lecture seule': 'readonly',
55
+ 'constructeur': '__construct',
56
+ 'destructeur': '__destruct',
57
+ 'vrai': 'true',
58
+ 'faux': 'false',
59
+ 'nul': 'null',
60
+ 'rien': 'null',
61
+ 'ceci': '$this',
62
+ 'self': 'self',
63
+ 'parent': 'parent',
64
+ 'echo': 'echo',
65
+ 'afficher': 'echo',
66
+ 'imprimer': 'print',
67
+ 'print': 'print',
68
+ 'inclure': 'include',
69
+ 'inclure une fois': 'include_once',
70
+ 'require': 'require',
71
+ 'requerir': 'require',
72
+ 'require une fois': 'require_once',
73
+ 'requerir une fois': 'require_once',
74
+ 'utiliser': 'use',
75
+ 'comme': 'as',
76
+ 'namespace': 'namespace',
77
+ 'espace de noms': 'namespace',
78
+ 'global': 'global',
79
+ 'instanceof': 'instanceof',
80
+ 'instance de': 'instanceof',
81
+ 'clone': 'clone',
82
+ 'cloner': 'clone',
83
+ 'rendement': 'yield',
84
+ 'rendement de': 'yield from',
85
+ 'fn': 'fn',
86
+ 'match': 'match',
87
+ 'enum': 'enum',
88
+ 'enumeration': 'enum',
89
+ 'addition': '+',
90
+ 'soustraction': '-',
91
+ 'multiplication': '*',
92
+ 'division': '/',
93
+ 'modulo': '%',
94
+ 'puissance': '**',
95
+ 'egal': '==',
96
+ 'strictement egal': '===',
97
+ 'different': '!=',
98
+ 'strictement different': '!==',
99
+ 'superieur': '>',
100
+ 'inferieur': '<',
101
+ 'superieur ou egal': '>=',
102
+ 'inferieur ou egal': '<=',
103
+ 'et': '&&',
104
+ 'ou': '||',
105
+ 'non': '!',
106
+ 'xor': 'xor',
107
+ 'coalescence': '??',
108
+ 'coalescence nulle': '??',
109
+ 'fleche': '->',
110
+ 'double fleche': '=>',
111
+ 'double deux points': '::',
112
+ 'resolution portee': '::',
113
+ 'concatener': '.',
114
+ 'concatenation': '.',
115
+ 'chaine': 'string',
116
+ 'entier': 'int',
117
+ 'flottant': 'float',
118
+ 'booleen': 'bool',
119
+ 'tableau': 'array',
120
+ 'objet': 'object',
121
+ 'mixte': 'mixed',
122
+ 'void': 'void',
123
+ 'jamais': 'never',
124
+ 'appelable': 'callable',
125
+ 'iterable': 'iterable',
126
+ 'longueur': 'strlen',
127
+ 'sous chaine': 'substr',
128
+ 'position': 'strpos',
129
+ 'remplacer': 'str_replace',
130
+ 'exploser': 'explode',
131
+ 'imploser': 'implode',
132
+ 'majuscules': 'strtoupper',
133
+ 'minuscules': 'strtolower',
134
+ 'rogner': 'trim',
135
+ 'compter': 'count',
136
+ 'trier': 'sort',
137
+ 'inverser': 'array_reverse',
138
+ 'fusionner': 'array_merge',
139
+ 'filtrer': 'array_filter',
140
+ 'transformer': 'array_map',
141
+ 'reduire': 'array_reduce',
142
+ 'cles': 'array_keys',
143
+ 'valeurs': 'array_values',
144
+ 'pousser': 'array_push',
145
+ 'retirer': 'array_pop',
146
+ 'rechercher': 'array_search',
147
+ 'existe': 'array_key_exists',
148
+ 'dans tableau': 'in_array',
149
+ 'ouvrir fichier': 'fopen',
150
+ 'fermer fichier': 'fclose',
151
+ 'lire fichier': 'fread',
152
+ 'ecrire fichier': 'fwrite',
153
+ 'contenu fichier': 'file_get_contents',
154
+ 'ecrire contenu': 'file_put_contents',
155
+ 'fichier existe': 'file_exists',
156
+ 'est fichier': 'is_file',
157
+ 'est repertoire': 'is_dir',
158
+ 'json encoder': 'json_encode',
159
+ 'json decoder': 'json_decode',
160
+ 'date': 'date',
161
+ 'temps': 'time',
162
+ 'maintenant': 'time',
163
+ 'demarrer session': 'session_start',
164
+ 'detruire session': 'session_destroy',
165
+ 'definir cookie': 'setcookie',
166
+ 'hacher': 'hash',
167
+ 'hacher mot de passe': 'password_hash',
168
+ 'verifier mot de passe': 'password_verify',
169
+ 'preparer': 'prepare',
170
+ 'executer': 'execute',
171
+ 'recuperer': 'fetch',
172
+ 'recuperer tout': 'fetchAll',
173
+ 'nombre lignes': 'rowCount',
174
+ 'dernier id': 'lastInsertId',
175
+ 'initialiser curl': 'curl_init',
176
+ 'executer curl': 'curl_exec',
177
+ 'fermer curl': 'curl_close',
178
+ 'option curl': 'curl_setopt',
179
+ 'envoyer mail': 'mail',
180
+ 'afficher info': 'var_dump',
181
+ 'imprimer r': 'print_r',
182
+ 'exporter': 'var_export',
183
+ 'vide': 'empty',
184
+ 'defini': 'isset',
185
+ 'supprimer': 'unset',
186
+ 'sortie': 'exit',
187
+ 'mourir': 'die',
188
+ 'dormir': 'sleep'
189
+ }
190
+
191
+ this.phpTypes = {
192
+ 'chaine': 'string',
193
+ 'string': 'string',
194
+ 'entier': 'int',
195
+ 'int': 'int',
196
+ 'integer': 'int',
197
+ 'flottant': 'float',
198
+ 'float': 'float',
199
+ 'double': 'float',
200
+ 'booleen': 'bool',
201
+ 'bool': 'bool',
202
+ 'boolean': 'bool',
203
+ 'tableau': 'array',
204
+ 'array': 'array',
205
+ 'objet': 'object',
206
+ 'object': 'object',
207
+ 'mixte': 'mixed',
208
+ 'mixed': 'mixed',
209
+ 'void': 'void',
210
+ 'nul': 'null',
211
+ 'null': 'null',
212
+ 'jamais': 'never',
213
+ 'never': 'never',
214
+ 'appelable': 'callable',
215
+ 'callable': 'callable',
216
+ 'iterable': 'iterable',
217
+ 'ressource': 'resource',
218
+ 'resource': 'resource',
219
+ 'self': 'self',
220
+ 'static': 'static',
221
+ 'statique': 'static',
222
+ 'parent': 'parent'
223
+ }
224
+
225
+ this.phpVisibility = ['public', 'publique', 'prive', 'privee', 'private', 'protege', 'protegee', 'protected']
226
+ this.phpModifiers = ['statique', 'static', 'final', 'finale', 'abstrait', 'abstraite', 'abstract', 'readonly', 'lecture seule']
227
+ }
228
+
229
+ translatePHP(word) {
230
+ if (!word) return word
231
+ const lower = this.normalizeAccents(String(word))
232
+ return this.phpTranslations[lower] || word
233
+ }
234
+
235
+ translateType(word) {
236
+ if (!word) return word
237
+ const lower = this.normalizeAccents(String(word))
238
+ return this.phpTypes[lower] || word
239
+ }
240
+
241
+ parse(source, lang = 'php') {
242
+ this.tokenize(source)
243
+ return this.parseProgram(lang)
244
+ }
245
+
246
+ parseProgram(lang) {
247
+ const program = {
248
+ type: 'Program',
249
+ lang: lang,
250
+ body: [],
251
+ phpTag: true
252
+ }
253
+
254
+ this.skipNewlines()
255
+
256
+ while (!this.isAtEnd()) {
257
+ const statement = this.parseStatement(lang)
258
+ if (statement) {
259
+ program.body.push(statement)
260
+ }
261
+ this.skipNewlines()
262
+ }
263
+
264
+ return program
265
+ }
266
+
267
+ parseStatement(lang) {
268
+ this.skipNewlines()
269
+ const token = this.current()
270
+
271
+ if (!token || token.type === TokenType.EOF) return null
272
+
273
+ const value = token.value != null ? this.normalizeAccents(String(token.value).toLowerCase()) : ''
274
+
275
+ if (this.isNamespace(value)) {
276
+ return this.parseNamespace(lang)
277
+ }
278
+
279
+ if (this.isUse(value)) {
280
+ return this.parseUseStatement(lang)
281
+ }
282
+
283
+ if (this.isClassDeclaration(value)) {
284
+ return this.parseClassDeclaration(lang)
285
+ }
286
+
287
+ if (this.isInterfaceDeclaration(value)) {
288
+ return this.parseInterfaceDeclaration(lang)
289
+ }
290
+
291
+ if (this.isTraitDeclaration(value)) {
292
+ return this.parseTraitDeclaration(lang)
293
+ }
294
+
295
+ if (this.isEnumDeclaration(value)) {
296
+ return this.parseEnumDeclaration(lang)
297
+ }
298
+
299
+ if (this.isFunctionDeclaration(value)) {
300
+ return this.parseFunctionDeclaration(lang)
301
+ }
302
+
303
+ if (this.isVariableDeclaration(value)) {
304
+ return this.parseVariableDeclaration(lang)
305
+ }
306
+
307
+ if (this.isConditional(value)) {
308
+ return this.parseConditional(lang)
309
+ }
310
+
311
+ if (this.isLoop(value)) {
312
+ return this.parseLoop(lang)
313
+ }
314
+
315
+ if (this.isSwitch(value)) {
316
+ return this.parseSwitch(lang)
317
+ }
318
+
319
+ if (this.isTry(value)) {
320
+ return this.parseTryStatement(lang)
321
+ }
322
+
323
+ if (this.isThrow(value)) {
324
+ return this.parseThrowStatement(lang)
325
+ }
326
+
327
+ if (this.isReturn(value)) {
328
+ return this.parseReturn(lang)
329
+ }
330
+
331
+ if (this.isEcho(value)) {
332
+ return this.parseEcho(lang)
333
+ }
334
+
335
+ if (this.isInclude(value)) {
336
+ return this.parseInclude(lang)
337
+ }
338
+
339
+ if (this.isGlobal(value)) {
340
+ return this.parseGlobalStatement(lang)
341
+ }
342
+
343
+ if (this.isMatch(value)) {
344
+ return this.parseMatchExpression(lang)
345
+ }
346
+
347
+ if (value === 'sortir' || value === 'break') {
348
+ this.advance()
349
+ return { type: 'BreakStatement' }
350
+ }
351
+
352
+ if (value === 'continuer' || value === 'continue') {
353
+ this.advance()
354
+ return { type: 'ContinueStatement' }
355
+ }
356
+
357
+ const expr = this.parseExpression(lang)
358
+ if (expr) {
359
+ return {
360
+ type: 'ExpressionStatement',
361
+ expression: expr
362
+ }
363
+ }
364
+ return null
365
+ }
366
+
367
+ isNamespace(value) {
368
+ return ['namespace', 'espace de noms'].includes(value)
369
+ }
370
+
371
+ isUse(value) {
372
+ return ['utiliser', 'use'].includes(value)
373
+ }
374
+
375
+ isClassDeclaration(value) {
376
+ const keywords = ['classe', 'class', 'abstrait', 'abstraite', 'abstract', 'final', 'finale', 'readonly']
377
+ if (keywords.includes(value)) {
378
+ if (['abstrait', 'abstraite', 'abstract', 'final', 'finale', 'readonly'].includes(value)) {
379
+ const next = this.peek(1)
380
+ if (next) {
381
+ const nextVal = this.normalizeAccents(String(next.value).toLowerCase())
382
+ return ['classe', 'class'].includes(nextVal)
383
+ }
384
+ }
385
+ return true
386
+ }
387
+ return false
388
+ }
389
+
390
+ isInterfaceDeclaration(value) {
391
+ return ['interface'].includes(value)
392
+ }
393
+
394
+ isTraitDeclaration(value) {
395
+ return ['trait'].includes(value)
396
+ }
397
+
398
+ isEnumDeclaration(value) {
399
+ return ['enum', 'enumeration'].includes(value)
400
+ }
401
+
402
+ isFunctionDeclaration(value) {
403
+ const keywords = ['fonction', 'function', 'fn', 'methode', 'procedure']
404
+ return keywords.includes(value)
405
+ }
406
+
407
+ isVariableDeclaration(value) {
408
+ const keywords = ['variable', 'constante', 'const', 'definir', 'define', 'statique', 'static', 'global']
409
+ return keywords.includes(value)
410
+ }
411
+
412
+ isConditional(value) {
413
+ return ['si', 'if'].includes(value)
414
+ }
415
+
416
+ isLoop(value) {
417
+ const keywords = ['pour', 'for', 'foreach', 'pour chaque', 'while', 'tant que', 'faire', 'do']
418
+ if (keywords.includes(value)) return true
419
+ if (value === 'tant') {
420
+ const next = this.peek(1)
421
+ if (next && this.normalizeAccents(String(next.value).toLowerCase()) === 'que') {
422
+ return true
423
+ }
424
+ }
425
+ if (value === 'pour') {
426
+ const next = this.peek(1)
427
+ if (next && this.normalizeAccents(String(next.value).toLowerCase()) === 'chaque') {
428
+ return true
429
+ }
430
+ }
431
+ return false
432
+ }
433
+
434
+ isSwitch(value) {
435
+ return ['selon', 'switch'].includes(value)
436
+ }
437
+
438
+ isTry(value) {
439
+ return ['essayer', 'try'].includes(value)
440
+ }
441
+
442
+ isThrow(value) {
443
+ return ['lancer', 'throw'].includes(value)
444
+ }
445
+
446
+ isReturn(value) {
447
+ return ['retourner', 'return', 'renvoyer'].includes(value)
448
+ }
449
+
450
+ isEcho(value) {
451
+ return ['echo', 'afficher', 'imprimer', 'print'].includes(value)
452
+ }
453
+
454
+ isInclude(value) {
455
+ return ['inclure', 'include', 'require', 'requerir', 'inclure une fois', 'include_once', 'require une fois', 'require_once', 'requerir une fois'].includes(value)
456
+ }
457
+
458
+ isGlobal(value) {
459
+ return value === 'global'
460
+ }
461
+
462
+ isMatch(value) {
463
+ return value === 'match'
464
+ }
465
+
466
+ parseNamespace(lang) {
467
+ this.advance()
468
+ const name = this.parseNamespacePath(lang)
469
+ this.skipNewlines()
470
+
471
+ let body = null
472
+ if (this.check(TokenType.LBRACE)) {
473
+ body = this.parseBlock(lang)
474
+ }
475
+
476
+ return {
477
+ type: 'NamespaceDeclaration',
478
+ name: name,
479
+ body: body
480
+ }
481
+ }
482
+
483
+ parseNamespacePath(lang) {
484
+ const parts = []
485
+
486
+ while (this.current() && this.current().type === TokenType.IDENTIFIER) {
487
+ parts.push(this.advance().value)
488
+ if (this.match(TokenType.BACKSLASH) || this.matchValue('\\')) {
489
+ continue
490
+ }
491
+ break
492
+ }
493
+
494
+ return parts.join('\\')
495
+ }
496
+
497
+ parseUseStatement(lang) {
498
+ this.advance()
499
+
500
+ let useType = null
501
+ const val = this.current() ? this.normalizeAccents(String(this.current().value).toLowerCase()) : ''
502
+ if (val === 'fonction' || val === 'function') {
503
+ useType = 'function'
504
+ this.advance()
505
+ } else if (val === 'constante' || val === 'const') {
506
+ useType = 'const'
507
+ this.advance()
508
+ }
509
+
510
+ const imports = []
511
+
512
+ do {
513
+ const path = this.parseNamespacePath(lang)
514
+ let alias = null
515
+
516
+ const asVal = this.current() ? this.normalizeAccents(String(this.current().value).toLowerCase()) : ''
517
+ if (asVal === 'comme' || asVal === 'as') {
518
+ this.advance()
519
+ alias = this.advance().value
520
+ }
521
+
522
+ imports.push({
523
+ path: path,
524
+ alias: alias
525
+ })
526
+ } while (this.match(TokenType.COMMA))
527
+
528
+ return {
529
+ type: 'UseStatement',
530
+ useType: useType,
531
+ imports: imports
532
+ }
533
+ }
534
+
535
+ parseClassDeclaration(lang) {
536
+ const modifiers = []
537
+
538
+ while (true) {
539
+ const val = this.current() ? this.normalizeAccents(String(this.current().value).toLowerCase()) : ''
540
+ if (['abstrait', 'abstraite', 'abstract'].includes(val)) {
541
+ modifiers.push('abstract')
542
+ this.advance()
543
+ } else if (['final', 'finale'].includes(val)) {
544
+ modifiers.push('final')
545
+ this.advance()
546
+ } else if (val === 'readonly') {
547
+ modifiers.push('readonly')
548
+ this.advance()
549
+ } else {
550
+ break
551
+ }
552
+ }
553
+
554
+ this.advance()
555
+
556
+ const nameToken = this.current()
557
+ const name = nameToken ? nameToken.value : 'AnonymousClass'
558
+ if (nameToken) this.advance()
559
+
560
+ let superClass = null
561
+ const extendsVal = this.current() ? this.normalizeAccents(String(this.current().value).toLowerCase()) : ''
562
+ if (['etendre', 'etend', 'extends'].includes(extendsVal)) {
563
+ this.advance()
564
+ superClass = this.parseNamespacePath(lang)
565
+ }
566
+
567
+ const interfaces = []
568
+ const implementsVal = this.current() ? this.normalizeAccents(String(this.current().value).toLowerCase()) : ''
569
+ if (['implementer', 'implemente', 'implements'].includes(implementsVal)) {
570
+ this.advance()
571
+ do {
572
+ interfaces.push(this.parseNamespacePath(lang))
573
+ } while (this.match(TokenType.COMMA))
574
+ }
575
+
576
+ this.skipNewlines()
577
+ const body = this.parseClassBody(lang)
578
+
579
+ return {
580
+ type: 'ClassDeclaration',
581
+ name: name,
582
+ modifiers: modifiers,
583
+ superClass: superClass,
584
+ interfaces: interfaces,
585
+ body: body
586
+ }
587
+ }
588
+
589
+ parseClassBody(lang) {
590
+ const members = []
591
+
592
+ this.match(TokenType.LBRACE)
593
+ this.skipNewlines()
594
+ this.match(TokenType.INDENT)
595
+
596
+ while (!this.isAtEnd()) {
597
+ this.skipNewlines()
598
+ const current = this.current()
599
+
600
+ if (!current) break
601
+ if (current.type === TokenType.RBRACE || current.type === TokenType.DEDENT) {
602
+ this.advance()
603
+ break
604
+ }
605
+
606
+ const member = this.parseClassMember(lang)
607
+ if (member) {
608
+ members.push(member)
609
+ }
610
+ this.skipNewlines()
611
+ }
612
+
613
+ this.match(TokenType.RBRACE)
614
+
615
+ return members
616
+ }
617
+
618
+ parseClassMember(lang) {
619
+ const modifiers = []
620
+ let visibility = null
621
+ let isStatic = false
622
+ let isReadonly = false
623
+ let isAbstract = false
624
+ let isFinal = false
625
+
626
+ while (true) {
627
+ const val = this.current() ? this.normalizeAccents(String(this.current().value).toLowerCase()) : ''
628
+
629
+ if (['public', 'publique'].includes(val)) {
630
+ visibility = 'public'
631
+ this.advance()
632
+ } else if (['prive', 'privee', 'private'].includes(val)) {
633
+ visibility = 'private'
634
+ this.advance()
635
+ } else if (['protege', 'protegee', 'protected'].includes(val)) {
636
+ visibility = 'protected'
637
+ this.advance()
638
+ } else if (['statique', 'static'].includes(val)) {
639
+ isStatic = true
640
+ this.advance()
641
+ } else if (val === 'readonly' || val === 'lecture seule') {
642
+ isReadonly = true
643
+ this.advance()
644
+ } else if (['abstrait', 'abstraite', 'abstract'].includes(val)) {
645
+ isAbstract = true
646
+ this.advance()
647
+ } else if (['final', 'finale'].includes(val)) {
648
+ isFinal = true
649
+ this.advance()
650
+ } else {
651
+ break
652
+ }
653
+ }
654
+
655
+ const val = this.current() ? this.normalizeAccents(String(this.current().value).toLowerCase()) : ''
656
+
657
+ if (['fonction', 'function', 'methode'].includes(val)) {
658
+ return this.parseMethodDeclaration(lang, visibility, isStatic, isAbstract, isFinal)
659
+ }
660
+
661
+ if (val === 'constante' || val === 'const') {
662
+ return this.parseClassConstant(lang, visibility)
663
+ }
664
+
665
+ if (val === 'utiliser' || val === 'use') {
666
+ return this.parseTraitUse(lang)
667
+ }
668
+
669
+ return this.parsePropertyDeclaration(lang, visibility, isStatic, isReadonly)
670
+ }
671
+
672
+ parseMethodDeclaration(lang, visibility, isStatic, isAbstract, isFinal) {
673
+ this.advance()
674
+
675
+ let isConstructor = false
676
+ let isDestructor = false
677
+ const nameToken = this.current()
678
+ let name = nameToken ? nameToken.value : 'anonyme'
679
+
680
+ const nameLower = this.normalizeAccents(name.toLowerCase())
681
+ if (nameLower === 'constructeur' || nameLower === '__construct') {
682
+ name = '__construct'
683
+ isConstructor = true
684
+ } else if (nameLower === 'destructeur' || nameLower === '__destruct') {
685
+ name = '__destruct'
686
+ isDestructor = true
687
+ }
688
+
689
+ if (nameToken) this.advance()
690
+
691
+ const params = this.parseParameters(lang)
692
+
693
+ let returnType = null
694
+ if (this.match(TokenType.COLON)) {
695
+ let nullable = false
696
+ if (this.match(TokenType.QUESTION)) {
697
+ nullable = true
698
+ }
699
+ returnType = this.parseTypeHint(lang)
700
+ if (returnType && nullable) {
701
+ returnType.nullable = true
702
+ }
703
+ }
704
+
705
+ this.skipNewlines()
706
+
707
+ let body = null
708
+ if (!isAbstract) {
709
+ body = this.parseBlock(lang)
710
+ }
711
+
712
+ return {
713
+ type: 'MethodDeclaration',
714
+ name: name,
715
+ visibility: visibility || 'public',
716
+ static: isStatic,
717
+ abstract: isAbstract,
718
+ final: isFinal,
719
+ constructor: isConstructor,
720
+ destructor: isDestructor,
721
+ params: params,
722
+ returnType: returnType,
723
+ body: body
724
+ }
725
+ }
726
+
727
+ parsePropertyDeclaration(lang, visibility, isStatic, isReadonly) {
728
+ let typeHint = null
729
+
730
+ const current = this.current()
731
+ if (current && current.type === TokenType.IDENTIFIER) {
732
+ const typeLower = this.normalizeAccents(String(current.value).toLowerCase())
733
+ if (this.phpTypes[typeLower]) {
734
+ typeHint = this.parseTypeHint(lang)
735
+ }
736
+ }
737
+
738
+ const nameToken = this.current()
739
+ let name = nameToken ? nameToken.value : 'property'
740
+
741
+ if (name.startsWith('$')) {
742
+ name = name.substring(1)
743
+ }
744
+
745
+ if (nameToken) this.advance()
746
+
747
+ let defaultValue = null
748
+ if (this.match(TokenType.EQUALS)) {
749
+ defaultValue = this.parseExpression(lang)
750
+ }
751
+
752
+ return {
753
+ type: 'PropertyDeclaration',
754
+ name: name,
755
+ visibility: visibility || 'public',
756
+ static: isStatic,
757
+ readonly: isReadonly,
758
+ typeHint: typeHint,
759
+ default: defaultValue
760
+ }
761
+ }
762
+
763
+ parseClassConstant(lang, visibility) {
764
+ this.advance()
765
+
766
+ const nameToken = this.current()
767
+ const name = nameToken ? nameToken.value : 'CONSTANT'
768
+ if (nameToken) this.advance()
769
+
770
+ this.match(TokenType.EQUALS)
771
+ const value = this.parseExpression(lang)
772
+
773
+ return {
774
+ type: 'ClassConstant',
775
+ name: name,
776
+ visibility: visibility || 'public',
777
+ value: value
778
+ }
779
+ }
780
+
781
+ parseTraitUse(lang) {
782
+ this.advance()
783
+ const traits = []
784
+
785
+ do {
786
+ traits.push(this.parseNamespacePath(lang))
787
+ } while (this.match(TokenType.COMMA))
788
+
789
+ let adaptations = []
790
+ if (this.match(TokenType.LBRACE)) {
791
+ while (!this.isAtEnd() && !this.check(TokenType.RBRACE)) {
792
+ this.skipNewlines()
793
+ adaptations.push(this.parseTraitAdaptation(lang))
794
+ this.match(TokenType.SEMICOLON)
795
+ this.skipNewlines()
796
+ }
797
+ this.match(TokenType.RBRACE)
798
+ }
799
+
800
+ return {
801
+ type: 'TraitUse',
802
+ traits: traits,
803
+ adaptations: adaptations
804
+ }
805
+ }
806
+
807
+ parseTraitAdaptation(lang) {
808
+ const first = this.parseNamespacePath(lang)
809
+
810
+ const val = this.current() ? this.normalizeAccents(String(this.current().value).toLowerCase()) : ''
811
+
812
+ if (val === 'insteadof' || val === 'au lieu de') {
813
+ this.advance()
814
+ const excluded = []
815
+ do {
816
+ excluded.push(this.parseNamespacePath(lang))
817
+ } while (this.match(TokenType.COMMA))
818
+
819
+ return {
820
+ type: 'TraitPrecedence',
821
+ method: first,
822
+ insteadof: excluded
823
+ }
824
+ }
825
+
826
+ if (val === 'comme' || val === 'as') {
827
+ this.advance()
828
+
829
+ let newVisibility = null
830
+ let newName = null
831
+
832
+ const nextVal = this.current() ? this.normalizeAccents(String(this.current().value).toLowerCase()) : ''
833
+ if (this.phpVisibility.includes(nextVal)) {
834
+ newVisibility = this.translatePHP(nextVal)
835
+ this.advance()
836
+ }
837
+
838
+ if (this.current() && this.current().type === TokenType.IDENTIFIER) {
839
+ newName = this.advance().value
840
+ }
841
+
842
+ return {
843
+ type: 'TraitAlias',
844
+ method: first,
845
+ newVisibility: newVisibility,
846
+ newName: newName
847
+ }
848
+ }
849
+
850
+ return { type: 'TraitAdaptation', value: first }
851
+ }
852
+
853
+ parseInterfaceDeclaration(lang) {
854
+ this.advance()
855
+
856
+ const nameToken = this.current()
857
+ const name = nameToken ? nameToken.value : 'AnonymousInterface'
858
+ if (nameToken) this.advance()
859
+
860
+ const extended = []
861
+ const extendsVal = this.current() ? this.normalizeAccents(String(this.current().value).toLowerCase()) : ''
862
+ if (['etendre', 'etend', 'extends'].includes(extendsVal)) {
863
+ this.advance()
864
+ do {
865
+ extended.push(this.parseNamespacePath(lang))
866
+ } while (this.match(TokenType.COMMA))
867
+ }
868
+
869
+ this.skipNewlines()
870
+ const body = this.parseClassBody(lang)
871
+
872
+ return {
873
+ type: 'InterfaceDeclaration',
874
+ name: name,
875
+ extends: extended,
876
+ body: body
877
+ }
878
+ }
879
+
880
+ parseTraitDeclaration(lang) {
881
+ this.advance()
882
+
883
+ const nameToken = this.current()
884
+ const name = nameToken ? nameToken.value : 'AnonymousTrait'
885
+ if (nameToken) this.advance()
886
+
887
+ this.skipNewlines()
888
+ const body = this.parseClassBody(lang)
889
+
890
+ return {
891
+ type: 'TraitDeclaration',
892
+ name: name,
893
+ body: body
894
+ }
895
+ }
896
+
897
+ parseEnumDeclaration(lang) {
898
+ this.advance()
899
+
900
+ const nameToken = this.current()
901
+ const name = nameToken ? nameToken.value : 'AnonymousEnum'
902
+ if (nameToken) this.advance()
903
+
904
+ let backingType = null
905
+ if (this.match(TokenType.COLON)) {
906
+ backingType = this.parseTypeHint(lang)
907
+ }
908
+
909
+ const interfaces = []
910
+ const implementsVal = this.current() ? this.normalizeAccents(String(this.current().value).toLowerCase()) : ''
911
+ if (['implementer', 'implemente', 'implements'].includes(implementsVal)) {
912
+ this.advance()
913
+ do {
914
+ interfaces.push(this.parseNamespacePath(lang))
915
+ } while (this.match(TokenType.COMMA))
916
+ }
917
+
918
+ this.skipNewlines()
919
+ const cases = this.parseEnumBody(lang)
920
+
921
+ return {
922
+ type: 'EnumDeclaration',
923
+ name: name,
924
+ backingType: backingType,
925
+ interfaces: interfaces,
926
+ cases: cases
927
+ }
928
+ }
929
+
930
+ parseEnumBody(lang) {
931
+ const members = []
932
+
933
+ this.match(TokenType.LBRACE)
934
+ this.skipNewlines()
935
+ this.match(TokenType.INDENT)
936
+
937
+ while (!this.isAtEnd()) {
938
+ this.skipNewlines()
939
+ const current = this.current()
940
+
941
+ if (!current) break
942
+ if (current.type === TokenType.RBRACE || current.type === TokenType.DEDENT) {
943
+ this.advance()
944
+ break
945
+ }
946
+
947
+ const val = this.normalizeAccents(String(current.value).toLowerCase())
948
+
949
+ if (val === 'cas' || val === 'case') {
950
+ this.advance()
951
+ const caseName = this.advance().value
952
+ let caseValue = null
953
+ if (this.match(TokenType.EQUALS)) {
954
+ caseValue = this.parseExpression(lang)
955
+ }
956
+ members.push({
957
+ type: 'EnumCase',
958
+ name: caseName,
959
+ value: caseValue
960
+ })
961
+ } else {
962
+ const member = this.parseClassMember(lang)
963
+ if (member) {
964
+ members.push(member)
965
+ }
966
+ }
967
+ this.skipNewlines()
968
+ }
969
+
970
+ this.match(TokenType.RBRACE)
971
+ return members
972
+ }
973
+
974
+ parseFunctionDeclaration(lang) {
975
+ const isArrow = this.current() && this.normalizeAccents(String(this.current().value).toLowerCase()) === 'fn'
976
+ this.advance()
977
+
978
+ let isReference = false
979
+ if (this.match(TokenType.AMPERSAND)) {
980
+ isReference = true
981
+ }
982
+
983
+ const nameToken = this.current()
984
+ const name = nameToken && nameToken.type === TokenType.IDENTIFIER ? nameToken.value : null
985
+ if (name) this.advance()
986
+
987
+ const params = this.parseParameters(lang)
988
+
989
+ let uses = []
990
+ const useVal = this.current() ? this.normalizeAccents(String(this.current().value).toLowerCase()) : ''
991
+ if (useVal === 'utiliser' || useVal === 'use') {
992
+ this.advance()
993
+ uses = this.parseClosureUse(lang)
994
+ }
995
+
996
+ let returnType = null
997
+ if (this.match(TokenType.COLON)) {
998
+ let nullable = false
999
+ if (this.match(TokenType.QUESTION)) {
1000
+ nullable = true
1001
+ }
1002
+ returnType = this.parseTypeHint(lang)
1003
+ if (returnType && nullable) {
1004
+ returnType.nullable = true
1005
+ }
1006
+ }
1007
+
1008
+ this.skipNewlines()
1009
+
1010
+ let body
1011
+ if (isArrow && this.match(TokenType.ARROW) || this.match(TokenType.DOUBLE_ARROW)) {
1012
+ body = {
1013
+ type: 'ReturnStatement',
1014
+ argument: this.parseExpression(lang)
1015
+ }
1016
+ return {
1017
+ type: 'ArrowFunctionDeclaration',
1018
+ name: name,
1019
+ params: params,
1020
+ returnType: returnType,
1021
+ body: body,
1022
+ reference: isReference
1023
+ }
1024
+ }
1025
+
1026
+ body = this.parseBlock(lang)
1027
+
1028
+ return {
1029
+ type: 'FunctionDeclaration',
1030
+ name: name,
1031
+ params: params,
1032
+ uses: uses,
1033
+ returnType: returnType,
1034
+ body: body,
1035
+ reference: isReference
1036
+ }
1037
+ }
1038
+
1039
+ parseClosureUse(lang) {
1040
+ const uses = []
1041
+ this.match(TokenType.LPAREN)
1042
+
1043
+ while (!this.isAtEnd() && !this.check(TokenType.RPAREN)) {
1044
+ let byRef = false
1045
+ if (this.match(TokenType.AMPERSAND)) {
1046
+ byRef = true
1047
+ }
1048
+
1049
+ const varToken = this.current()
1050
+ let varName = varToken ? varToken.value : ''
1051
+ if (varName.startsWith('$')) {
1052
+ varName = varName.substring(1)
1053
+ }
1054
+ this.advance()
1055
+
1056
+ uses.push({
1057
+ name: varName,
1058
+ byReference: byRef
1059
+ })
1060
+
1061
+ this.match(TokenType.COMMA)
1062
+ }
1063
+
1064
+ this.match(TokenType.RPAREN)
1065
+ return uses
1066
+ }
1067
+
1068
+ parseParameters(lang) {
1069
+ const params = []
1070
+ this.match(TokenType.LPAREN)
1071
+
1072
+ while (!this.isAtEnd() && !this.check(TokenType.RPAREN)) {
1073
+ this.skipNewlines()
1074
+ if (this.check(TokenType.RPAREN)) break
1075
+
1076
+ const param = this.parseParameterPHP(lang)
1077
+ if (param) {
1078
+ params.push(param)
1079
+ }
1080
+ this.match(TokenType.COMMA)
1081
+ this.skipNewlines()
1082
+ }
1083
+
1084
+ this.match(TokenType.RPAREN)
1085
+ return params
1086
+ }
1087
+
1088
+ parseParameterPHP(lang) {
1089
+ let visibility = null
1090
+ let readonly = false
1091
+ let typeHint = null
1092
+ let variadic = false
1093
+ let byReference = false
1094
+ let defaultValue = null
1095
+
1096
+ const val = this.current() ? this.normalizeAccents(String(this.current().value).toLowerCase()) : ''
1097
+ if (this.phpVisibility.includes(val)) {
1098
+ visibility = this.translatePHP(val)
1099
+ this.advance()
1100
+ }
1101
+
1102
+ const readonlyVal = this.current() ? this.normalizeAccents(String(this.current().value).toLowerCase()) : ''
1103
+ if (readonlyVal === 'readonly' || readonlyVal === 'lecture seule') {
1104
+ readonly = true
1105
+ this.advance()
1106
+ }
1107
+
1108
+ const currentType = this.current()
1109
+ if (currentType && currentType.type === TokenType.IDENTIFIER) {
1110
+ const typeLower = this.normalizeAccents(String(currentType.value).toLowerCase())
1111
+ if (this.phpTypes[typeLower] || typeLower[0] === typeLower[0].toUpperCase()) {
1112
+ if (!this.peek(1) || this.peek(1).type !== TokenType.EQUALS) {
1113
+ typeHint = this.parseTypeHint(lang)
1114
+ }
1115
+ }
1116
+ }
1117
+
1118
+ if (this.match(TokenType.SPREAD) || this.matchValue('...')) {
1119
+ variadic = true
1120
+ }
1121
+
1122
+ if (this.match(TokenType.AMPERSAND)) {
1123
+ byReference = true
1124
+ }
1125
+
1126
+ const nameToken = this.current()
1127
+ if (!nameToken) return null
1128
+
1129
+ let name = nameToken.value
1130
+ if (name.startsWith('$')) {
1131
+ name = name.substring(1)
1132
+ }
1133
+ this.advance()
1134
+
1135
+ if (this.match(TokenType.EQUALS)) {
1136
+ defaultValue = this.parseExpression(lang)
1137
+ }
1138
+
1139
+ return {
1140
+ type: 'Parameter',
1141
+ name: name,
1142
+ typeHint: typeHint,
1143
+ visibility: visibility,
1144
+ readonly: readonly,
1145
+ variadic: variadic,
1146
+ byReference: byReference,
1147
+ default: defaultValue
1148
+ }
1149
+ }
1150
+
1151
+ parseTypeHint(lang) {
1152
+ let nullable = false
1153
+ if (this.match(TokenType.QUESTION)) {
1154
+ nullable = true
1155
+ }
1156
+
1157
+ const types = []
1158
+
1159
+ do {
1160
+ const typeToken = this.current()
1161
+ if (!typeToken || typeToken.type !== TokenType.IDENTIFIER) break
1162
+
1163
+ const typeName = this.translateType(typeToken.value)
1164
+ this.advance()
1165
+ types.push(typeName)
1166
+ } while (this.match(TokenType.PIPE))
1167
+
1168
+ if (types.length === 0) return null
1169
+
1170
+ return {
1171
+ type: 'TypeHint',
1172
+ types: types,
1173
+ nullable: nullable,
1174
+ union: types.length > 1
1175
+ }
1176
+ }
1177
+
1178
+ parseVariableDeclaration(lang) {
1179
+ const kindToken = this.advance()
1180
+ const kindLower = this.normalizeAccents(String(kindToken.value).toLowerCase())
1181
+
1182
+ if (kindLower === 'constante' || kindLower === 'const') {
1183
+ const nameToken = this.current()
1184
+ const name = nameToken ? nameToken.value : 'CONSTANT'
1185
+ if (nameToken) this.advance()
1186
+
1187
+ this.match(TokenType.EQUALS)
1188
+ const value = this.parseExpression(lang)
1189
+
1190
+ return {
1191
+ type: 'ConstantDeclaration',
1192
+ name: name,
1193
+ value: value
1194
+ }
1195
+ }
1196
+
1197
+ if (kindLower === 'definir' || kindLower === 'define') {
1198
+ this.match(TokenType.LPAREN)
1199
+ const nameExpr = this.parseExpression(lang)
1200
+ this.match(TokenType.COMMA)
1201
+ const value = this.parseExpression(lang)
1202
+ this.match(TokenType.RPAREN)
1203
+
1204
+ return {
1205
+ type: 'DefineDeclaration',
1206
+ name: nameExpr,
1207
+ value: value
1208
+ }
1209
+ }
1210
+
1211
+ let typeHint = null
1212
+ const currentType = this.current()
1213
+ if (currentType && currentType.type === TokenType.IDENTIFIER) {
1214
+ const typeLower = this.normalizeAccents(String(currentType.value).toLowerCase())
1215
+ if (this.phpTypes[typeLower]) {
1216
+ typeHint = this.parseTypeHint(lang)
1217
+ }
1218
+ }
1219
+
1220
+ const nameToken = this.current()
1221
+ let name = nameToken ? nameToken.value : 'variable'
1222
+ if (name.startsWith('$')) {
1223
+ name = name.substring(1)
1224
+ }
1225
+ if (nameToken) this.advance()
1226
+
1227
+ let initializer = null
1228
+ if (this.match(TokenType.EQUALS)) {
1229
+ initializer = this.parseExpression(lang)
1230
+ }
1231
+
1232
+ return {
1233
+ type: 'VariableDeclaration',
1234
+ kind: kindLower === 'statique' || kindLower === 'static' ? 'static' : 'var',
1235
+ name: name,
1236
+ typeHint: typeHint,
1237
+ init: initializer
1238
+ }
1239
+ }
1240
+
1241
+ parseConditional(lang) {
1242
+ this.advance()
1243
+
1244
+ let condition
1245
+ if (this.match(TokenType.LPAREN)) {
1246
+ condition = this.parseExpression(lang)
1247
+ this.match(TokenType.RPAREN)
1248
+ } else {
1249
+ condition = this.parseExpression(lang)
1250
+ }
1251
+
1252
+ this.skipNewlines()
1253
+ const consequent = this.parseBlock(lang)
1254
+
1255
+ let alternate = null
1256
+ this.skipNewlines()
1257
+
1258
+ const elseVal = this.current() ? this.normalizeAccents(String(this.current().value).toLowerCase()) : ''
1259
+ if (elseVal === 'sinon si' || elseVal === 'elseif') {
1260
+ this.advance()
1261
+ alternate = this.parseConditional(lang)
1262
+ alternate.type = 'ElseIfStatement'
1263
+ } else if (elseVal === 'sinon' || elseVal === 'else') {
1264
+ this.advance()
1265
+
1266
+ const nextVal = this.current() ? this.normalizeAccents(String(this.current().value).toLowerCase()) : ''
1267
+ if (nextVal === 'si' || nextVal === 'if') {
1268
+ alternate = this.parseConditional(lang)
1269
+ alternate.type = 'ElseIfStatement'
1270
+ } else {
1271
+ this.skipNewlines()
1272
+ alternate = this.parseBlock(lang)
1273
+ }
1274
+ }
1275
+
1276
+ return {
1277
+ type: 'IfStatement',
1278
+ condition: condition,
1279
+ consequent: consequent,
1280
+ alternate: alternate
1281
+ }
1282
+ }
1283
+
1284
+ parseLoop(lang) {
1285
+ const token = this.current()
1286
+ const value = this.normalizeAccents(String(token.value).toLowerCase())
1287
+
1288
+ if (value === 'pour') {
1289
+ const next = this.peek(1)
1290
+ if (next && this.normalizeAccents(String(next.value).toLowerCase()) === 'chaque') {
1291
+ this.advance()
1292
+ this.advance()
1293
+ return this.parseForeach(lang)
1294
+ }
1295
+ return this.parseFor(lang)
1296
+ }
1297
+
1298
+ if (value === 'foreach' || value === 'pour chaque') {
1299
+ this.advance()
1300
+ if (value === 'pour chaque') this.advance()
1301
+ return this.parseForeach(lang)
1302
+ }
1303
+
1304
+ if (value === 'while' || value === 'tant') {
1305
+ this.advance()
1306
+ if (value === 'tant') {
1307
+ this.matchValue('que')
1308
+ }
1309
+ return this.parseWhile(lang)
1310
+ }
1311
+
1312
+ if (value === 'faire' || value === 'do') {
1313
+ return this.parseDoWhile(lang)
1314
+ }
1315
+
1316
+ this.advance()
1317
+ return this.parseFor(lang)
1318
+ }
1319
+
1320
+ parseFor(lang) {
1321
+ this.advance()
1322
+
1323
+ let init = null
1324
+ let test = null
1325
+ let update = null
1326
+
1327
+ if (this.match(TokenType.LPAREN)) {
1328
+ if (!this.check(TokenType.SEMICOLON)) {
1329
+ init = this.parseExpression(lang)
1330
+ }
1331
+ this.match(TokenType.SEMICOLON)
1332
+
1333
+ if (!this.check(TokenType.SEMICOLON)) {
1334
+ test = this.parseExpression(lang)
1335
+ }
1336
+ this.match(TokenType.SEMICOLON)
1337
+
1338
+ if (!this.check(TokenType.RPAREN)) {
1339
+ update = this.parseExpression(lang)
1340
+ }
1341
+ this.match(TokenType.RPAREN)
1342
+ }
1343
+
1344
+ this.skipNewlines()
1345
+ const body = this.parseBlock(lang)
1346
+
1347
+ return {
1348
+ type: 'ForStatement',
1349
+ init: init,
1350
+ test: test,
1351
+ update: update,
1352
+ body: body
1353
+ }
1354
+ }
1355
+
1356
+ parseForeach(lang) {
1357
+ let array = null
1358
+ let key = null
1359
+ let value = null
1360
+ let byRef = false
1361
+
1362
+ if (this.match(TokenType.LPAREN)) {
1363
+ array = this.parseExpression(lang)
1364
+
1365
+ const asVal = this.current() ? this.normalizeAccents(String(this.current().value).toLowerCase()) : ''
1366
+ if (asVal === 'comme' || asVal === 'as') {
1367
+ this.advance()
1368
+ }
1369
+
1370
+ if (this.match(TokenType.AMPERSAND)) {
1371
+ byRef = true
1372
+ }
1373
+
1374
+ const firstVar = this.current()
1375
+ let firstName = firstVar ? firstVar.value : 'item'
1376
+ if (firstName.startsWith('$')) firstName = firstName.substring(1)
1377
+ this.advance()
1378
+
1379
+ if (this.match(TokenType.DOUBLE_ARROW) || this.matchValue('=>')) {
1380
+ key = firstName
1381
+
1382
+ if (this.match(TokenType.AMPERSAND)) {
1383
+ byRef = true
1384
+ }
1385
+
1386
+ const valueVar = this.current()
1387
+ value = valueVar ? valueVar.value : 'value'
1388
+ if (value.startsWith('$')) value = value.substring(1)
1389
+ this.advance()
1390
+ } else {
1391
+ value = firstName
1392
+ }
1393
+
1394
+ this.match(TokenType.RPAREN)
1395
+ }
1396
+
1397
+ this.skipNewlines()
1398
+ const body = this.parseBlock(lang)
1399
+
1400
+ return {
1401
+ type: 'ForeachStatement',
1402
+ array: array,
1403
+ key: key,
1404
+ value: value,
1405
+ byReference: byRef,
1406
+ body: body
1407
+ }
1408
+ }
1409
+
1410
+ parseWhile(lang) {
1411
+ let condition
1412
+ if (this.match(TokenType.LPAREN)) {
1413
+ condition = this.parseExpression(lang)
1414
+ this.match(TokenType.RPAREN)
1415
+ } else {
1416
+ condition = this.parseExpression(lang)
1417
+ }
1418
+
1419
+ this.skipNewlines()
1420
+ const body = this.parseBlock(lang)
1421
+
1422
+ return {
1423
+ type: 'WhileStatement',
1424
+ condition: condition,
1425
+ body: body
1426
+ }
1427
+ }
1428
+
1429
+ parseDoWhile(lang) {
1430
+ this.advance()
1431
+
1432
+ this.skipNewlines()
1433
+ const body = this.parseBlock(lang)
1434
+
1435
+ this.skipNewlines()
1436
+ const whileVal = this.current() ? this.normalizeAccents(String(this.current().value).toLowerCase()) : ''
1437
+ if (whileVal === 'while' || whileVal === 'tant') {
1438
+ this.advance()
1439
+ if (whileVal === 'tant') {
1440
+ this.matchValue('que')
1441
+ }
1442
+ }
1443
+
1444
+ let condition
1445
+ if (this.match(TokenType.LPAREN)) {
1446
+ condition = this.parseExpression(lang)
1447
+ this.match(TokenType.RPAREN)
1448
+ } else {
1449
+ condition = this.parseExpression(lang)
1450
+ }
1451
+
1452
+ return {
1453
+ type: 'DoWhileStatement',
1454
+ body: body,
1455
+ condition: condition
1456
+ }
1457
+ }
1458
+
1459
+ parseSwitch(lang) {
1460
+ this.advance()
1461
+
1462
+ let discriminant
1463
+ if (this.match(TokenType.LPAREN)) {
1464
+ discriminant = this.parseExpression(lang)
1465
+ this.match(TokenType.RPAREN)
1466
+ } else {
1467
+ discriminant = this.parseExpression(lang)
1468
+ }
1469
+
1470
+ this.skipNewlines()
1471
+ const cases = this.parseSwitchBody(lang)
1472
+
1473
+ return {
1474
+ type: 'SwitchStatement',
1475
+ discriminant: discriminant,
1476
+ cases: cases
1477
+ }
1478
+ }
1479
+
1480
+ parseSwitchBody(lang) {
1481
+ const cases = []
1482
+
1483
+ this.match(TokenType.LBRACE)
1484
+ this.skipNewlines()
1485
+ this.match(TokenType.INDENT)
1486
+
1487
+ while (!this.isAtEnd()) {
1488
+ this.skipNewlines()
1489
+ const current = this.current()
1490
+
1491
+ if (!current) break
1492
+ if (current.type === TokenType.RBRACE || current.type === TokenType.DEDENT) {
1493
+ this.advance()
1494
+ break
1495
+ }
1496
+
1497
+ const val = this.normalizeAccents(String(current.value).toLowerCase())
1498
+
1499
+ if (val === 'cas' || val === 'case') {
1500
+ this.advance()
1501
+ const test = this.parseExpression(lang)
1502
+ this.match(TokenType.COLON)
1503
+ this.skipNewlines()
1504
+
1505
+ const consequent = []
1506
+ while (!this.isAtEnd()) {
1507
+ const nextVal = this.current() ? this.normalizeAccents(String(this.current().value).toLowerCase()) : ''
1508
+ if (['cas', 'case', 'defaut', 'default'].includes(nextVal) ||
1509
+ this.current().type === TokenType.RBRACE ||
1510
+ this.current().type === TokenType.DEDENT) {
1511
+ break
1512
+ }
1513
+ const stmt = this.parseStatement(lang)
1514
+ if (stmt) consequent.push(stmt)
1515
+ this.skipNewlines()
1516
+ }
1517
+
1518
+ cases.push({
1519
+ type: 'SwitchCase',
1520
+ test: test,
1521
+ consequent: consequent
1522
+ })
1523
+ } else if (val === 'defaut' || val === 'default') {
1524
+ this.advance()
1525
+ this.match(TokenType.COLON)
1526
+ this.skipNewlines()
1527
+
1528
+ const consequent = []
1529
+ while (!this.isAtEnd()) {
1530
+ if (this.current().type === TokenType.RBRACE || this.current().type === TokenType.DEDENT) {
1531
+ break
1532
+ }
1533
+ const stmt = this.parseStatement(lang)
1534
+ if (stmt) consequent.push(stmt)
1535
+ this.skipNewlines()
1536
+ }
1537
+
1538
+ cases.push({
1539
+ type: 'SwitchCase',
1540
+ test: null,
1541
+ consequent: consequent
1542
+ })
1543
+ } else {
1544
+ break
1545
+ }
1546
+ }
1547
+
1548
+ this.match(TokenType.RBRACE)
1549
+ return cases
1550
+ }
1551
+
1552
+ parseMatchExpression(lang) {
1553
+ this.advance()
1554
+
1555
+ let subject
1556
+ if (this.match(TokenType.LPAREN)) {
1557
+ subject = this.parseExpression(lang)
1558
+ this.match(TokenType.RPAREN)
1559
+ } else {
1560
+ subject = this.parseExpression(lang)
1561
+ }
1562
+
1563
+ this.skipNewlines()
1564
+ const arms = this.parseMatchArms(lang)
1565
+
1566
+ return {
1567
+ type: 'MatchExpression',
1568
+ subject: subject,
1569
+ arms: arms
1570
+ }
1571
+ }
1572
+
1573
+ parseMatchArms(lang) {
1574
+ const arms = []
1575
+
1576
+ this.match(TokenType.LBRACE)
1577
+ this.skipNewlines()
1578
+
1579
+ while (!this.isAtEnd() && !this.check(TokenType.RBRACE)) {
1580
+ this.skipNewlines()
1581
+
1582
+ const conditions = []
1583
+ let isDefault = false
1584
+
1585
+ const val = this.current() ? this.normalizeAccents(String(this.current().value).toLowerCase()) : ''
1586
+ if (val === 'defaut' || val === 'default') {
1587
+ isDefault = true
1588
+ this.advance()
1589
+ } else {
1590
+ do {
1591
+ conditions.push(this.parseExpression(lang))
1592
+ } while (this.match(TokenType.COMMA))
1593
+ }
1594
+
1595
+ this.match(TokenType.DOUBLE_ARROW) || this.matchValue('=>')
1596
+ const body = this.parseExpression(lang)
1597
+
1598
+ arms.push({
1599
+ type: 'MatchArm',
1600
+ conditions: isDefault ? null : conditions,
1601
+ default: isDefault,
1602
+ body: body
1603
+ })
1604
+
1605
+ this.match(TokenType.COMMA)
1606
+ this.skipNewlines()
1607
+ }
1608
+
1609
+ this.match(TokenType.RBRACE)
1610
+ return arms
1611
+ }
1612
+
1613
+ parseTryStatement(lang) {
1614
+ this.advance()
1615
+
1616
+ this.skipNewlines()
1617
+ const tryBlock = this.parseBlock(lang)
1618
+
1619
+ const catches = []
1620
+ this.skipNewlines()
1621
+
1622
+ while (true) {
1623
+ const catchVal = this.current() ? this.normalizeAccents(String(this.current().value).toLowerCase()) : ''
1624
+ if (catchVal !== 'attraper' && catchVal !== 'catch') break
1625
+
1626
+ this.advance()
1627
+
1628
+ let types = []
1629
+ let param = null
1630
+
1631
+ if (this.match(TokenType.LPAREN)) {
1632
+ do {
1633
+ const typeToken = this.current()
1634
+ if (typeToken && typeToken.type === TokenType.IDENTIFIER) {
1635
+ const typeName = typeToken.value
1636
+ if (typeName.startsWith('$')) {
1637
+ param = typeName.substring(1)
1638
+ } else {
1639
+ types.push(typeName)
1640
+ this.advance()
1641
+ }
1642
+ }
1643
+ } while (this.match(TokenType.PIPE))
1644
+
1645
+ if (!param) {
1646
+ const paramToken = this.current()
1647
+ if (paramToken && paramToken.type === TokenType.IDENTIFIER) {
1648
+ param = paramToken.value
1649
+ if (param.startsWith('$')) param = param.substring(1)
1650
+ this.advance()
1651
+ }
1652
+ }
1653
+ this.match(TokenType.RPAREN)
1654
+ }
1655
+
1656
+ this.skipNewlines()
1657
+ const catchBlock = this.parseBlock(lang)
1658
+
1659
+ catches.push({
1660
+ type: 'CatchClause',
1661
+ types: types,
1662
+ param: param,
1663
+ body: catchBlock
1664
+ })
1665
+
1666
+ this.skipNewlines()
1667
+ }
1668
+
1669
+ let finallyBlock = null
1670
+ const finallyVal = this.current() ? this.normalizeAccents(String(this.current().value).toLowerCase()) : ''
1671
+ if (finallyVal === 'finalement' || finallyVal === 'finally') {
1672
+ this.advance()
1673
+ this.skipNewlines()
1674
+ finallyBlock = this.parseBlock(lang)
1675
+ }
1676
+
1677
+ return {
1678
+ type: 'TryStatement',
1679
+ block: tryBlock,
1680
+ catches: catches,
1681
+ finally: finallyBlock
1682
+ }
1683
+ }
1684
+
1685
+ parseThrowStatement(lang) {
1686
+ this.advance()
1687
+ const argument = this.parseExpression(lang)
1688
+
1689
+ return {
1690
+ type: 'ThrowStatement',
1691
+ argument: argument
1692
+ }
1693
+ }
1694
+
1695
+ parseReturn(lang) {
1696
+ this.advance()
1697
+
1698
+ let argument = null
1699
+ if (!this.isAtEnd() &&
1700
+ !this.check(TokenType.NEWLINE) &&
1701
+ !this.check(TokenType.SEMICOLON) &&
1702
+ !this.check(TokenType.DEDENT)) {
1703
+ argument = this.parseExpression(lang)
1704
+ }
1705
+
1706
+ return {
1707
+ type: 'ReturnStatement',
1708
+ argument: argument
1709
+ }
1710
+ }
1711
+
1712
+ parseEcho(lang) {
1713
+ const token = this.advance()
1714
+ const isPrint = this.normalizeAccents(String(token.value).toLowerCase()) === 'print' ||
1715
+ this.normalizeAccents(String(token.value).toLowerCase()) === 'imprimer'
1716
+
1717
+ const expressions = []
1718
+ do {
1719
+ expressions.push(this.parseExpression(lang))
1720
+ } while (this.match(TokenType.COMMA))
1721
+
1722
+ return {
1723
+ type: isPrint ? 'PrintStatement' : 'EchoStatement',
1724
+ expressions: expressions
1725
+ }
1726
+ }
1727
+
1728
+ parseInclude(lang) {
1729
+ const token = this.advance()
1730
+ const val = this.normalizeAccents(String(token.value).toLowerCase())
1731
+
1732
+ let includeType = 'include'
1733
+ if (val === 'require' || val === 'requerir') {
1734
+ includeType = 'require'
1735
+ } else if (val === 'inclure une fois' || val === 'include_once') {
1736
+ includeType = 'include_once'
1737
+ } else if (val === 'require une fois' || val === 'require_once' || val === 'requerir une fois') {
1738
+ includeType = 'require_once'
1739
+ }
1740
+
1741
+ let path
1742
+ if (this.match(TokenType.LPAREN)) {
1743
+ path = this.parseExpression(lang)
1744
+ this.match(TokenType.RPAREN)
1745
+ } else {
1746
+ path = this.parseExpression(lang)
1747
+ }
1748
+
1749
+ return {
1750
+ type: 'IncludeStatement',
1751
+ includeType: includeType,
1752
+ path: path
1753
+ }
1754
+ }
1755
+
1756
+ parseGlobalStatement(lang) {
1757
+ this.advance()
1758
+
1759
+ const variables = []
1760
+ do {
1761
+ const varToken = this.current()
1762
+ let varName = varToken ? varToken.value : ''
1763
+ if (varName.startsWith('$')) varName = varName.substring(1)
1764
+ this.advance()
1765
+ variables.push(varName)
1766
+ } while (this.match(TokenType.COMMA))
1767
+
1768
+ return {
1769
+ type: 'GlobalStatement',
1770
+ variables: variables
1771
+ }
1772
+ }
1773
+
1774
+ parseExpression(lang) {
1775
+ return this.parseAssignment(lang)
1776
+ }
1777
+
1778
+ parseAssignment(lang) {
1779
+ let left = this.parseTernary(lang)
1780
+
1781
+ if (this.match(TokenType.EQUALS)) {
1782
+ const right = this.parseAssignment(lang)
1783
+ return { type: 'AssignmentExpression', operator: '=', left, right }
1784
+ }
1785
+ if (this.match(TokenType.PLUS_EQUALS)) {
1786
+ const right = this.parseAssignment(lang)
1787
+ return { type: 'AssignmentExpression', operator: '+=', left, right }
1788
+ }
1789
+ if (this.match(TokenType.MINUS_EQUALS)) {
1790
+ const right = this.parseAssignment(lang)
1791
+ return { type: 'AssignmentExpression', operator: '-=', left, right }
1792
+ }
1793
+ if (this.match(TokenType.STAR_EQUALS)) {
1794
+ const right = this.parseAssignment(lang)
1795
+ return { type: 'AssignmentExpression', operator: '*=', left, right }
1796
+ }
1797
+ if (this.match(TokenType.SLASH_EQUALS)) {
1798
+ const right = this.parseAssignment(lang)
1799
+ return { type: 'AssignmentExpression', operator: '/=', left, right }
1800
+ }
1801
+ if (this.match(TokenType.PERCENT_EQUALS)) {
1802
+ const right = this.parseAssignment(lang)
1803
+ return { type: 'AssignmentExpression', operator: '%=', left, right }
1804
+ }
1805
+ if (this.match(TokenType.DOT_EQUALS) || this.matchValue('.=')) {
1806
+ const right = this.parseAssignment(lang)
1807
+ return { type: 'AssignmentExpression', operator: '.=', left, right }
1808
+ }
1809
+ if (this.match(TokenType.DOUBLE_QUESTION_EQUALS) || this.matchValue('??=')) {
1810
+ const right = this.parseAssignment(lang)
1811
+ return { type: 'AssignmentExpression', operator: '??=', left, right }
1812
+ }
1813
+
1814
+ return left
1815
+ }
1816
+
1817
+ parseTernary(lang) {
1818
+ let condition = this.parseNullCoalesce(lang)
1819
+
1820
+ if (this.match(TokenType.QUESTION)) {
1821
+ let consequent = null
1822
+ if (!this.check(TokenType.COLON)) {
1823
+ consequent = this.parseExpression(lang)
1824
+ }
1825
+ this.match(TokenType.COLON)
1826
+ const alternate = this.parseTernary(lang)
1827
+ return { type: 'ConditionalExpression', condition, consequent, alternate }
1828
+ }
1829
+
1830
+ return condition
1831
+ }
1832
+
1833
+ parseNullCoalesce(lang) {
1834
+ let left = this.parseLogicalOr(lang)
1835
+
1836
+ while (this.match(TokenType.DOUBLE_QUESTION) || this.matchValue('??') || this.matchValue('coalescence')) {
1837
+ const right = this.parseLogicalOr(lang)
1838
+ left = { type: 'BinaryExpression', operator: '??', left, right }
1839
+ }
1840
+
1841
+ return left
1842
+ }
1843
+
1844
+ parseLogicalOr(lang) {
1845
+ let left = this.parseLogicalAnd(lang)
1846
+
1847
+ while (this.match(TokenType.OR) || this.matchValue('ou') || this.matchValue('or')) {
1848
+ const right = this.parseLogicalAnd(lang)
1849
+ left = { type: 'BinaryExpression', operator: '||', left, right }
1850
+ }
1851
+
1852
+ return left
1853
+ }
1854
+
1855
+ parseLogicalAnd(lang) {
1856
+ let left = this.parseEquality(lang)
1857
+
1858
+ while (this.match(TokenType.AND) || this.matchValue('et') || this.matchValue('and')) {
1859
+ const right = this.parseEquality(lang)
1860
+ left = { type: 'BinaryExpression', operator: '&&', left, right }
1861
+ }
1862
+
1863
+ return left
1864
+ }
1865
+
1866
+ parseEquality(lang) {
1867
+ let left = this.parseComparison(lang)
1868
+
1869
+ while (true) {
1870
+ if (this.match(TokenType.TRIPLE_EQUALS) || this.matchValue('strictement egal') || this.matchValue('===')) {
1871
+ const right = this.parseComparison(lang)
1872
+ left = { type: 'BinaryExpression', operator: '===', left, right }
1873
+ } else if (this.match(TokenType.BANG_DOUBLE_EQUALS) || this.matchValue('strictement different') || this.matchValue('!==')) {
1874
+ const right = this.parseComparison(lang)
1875
+ left = { type: 'BinaryExpression', operator: '!==', left, right }
1876
+ } else if (this.match(TokenType.DOUBLE_EQUALS) || this.matchValue('egal') || this.matchValue('==')) {
1877
+ const right = this.parseComparison(lang)
1878
+ left = { type: 'BinaryExpression', operator: '==', left, right }
1879
+ } else if (this.match(TokenType.BANG_EQUALS) || this.matchValue('different') || this.matchValue('!=')) {
1880
+ const right = this.parseComparison(lang)
1881
+ left = { type: 'BinaryExpression', operator: '!=', left, right }
1882
+ } else if (this.match(TokenType.SPACESHIP) || this.matchValue('<=>')) {
1883
+ const right = this.parseComparison(lang)
1884
+ left = { type: 'BinaryExpression', operator: '<=>', left, right }
1885
+ } else {
1886
+ break
1887
+ }
1888
+ }
1889
+
1890
+ return left
1891
+ }
1892
+
1893
+ parseComparison(lang) {
1894
+ let left = this.parseConcatenation(lang)
1895
+
1896
+ while (true) {
1897
+ if (this.match(TokenType.LT) || this.matchValue('inferieur') || this.matchValue('<')) {
1898
+ const right = this.parseConcatenation(lang)
1899
+ left = { type: 'BinaryExpression', operator: '<', left, right }
1900
+ } else if (this.match(TokenType.GT) || this.matchValue('superieur') || this.matchValue('>')) {
1901
+ const right = this.parseConcatenation(lang)
1902
+ left = { type: 'BinaryExpression', operator: '>', left, right }
1903
+ } else if (this.match(TokenType.LTE) || this.matchValue('inferieur ou egal') || this.matchValue('<=')) {
1904
+ const right = this.parseConcatenation(lang)
1905
+ left = { type: 'BinaryExpression', operator: '<=', left, right }
1906
+ } else if (this.match(TokenType.GTE) || this.matchValue('superieur ou egal') || this.matchValue('>=')) {
1907
+ const right = this.parseConcatenation(lang)
1908
+ left = { type: 'BinaryExpression', operator: '>=', left, right }
1909
+ } else if (this.matchValue('instanceof') || this.matchValue('instance de')) {
1910
+ const right = this.parseConcatenation(lang)
1911
+ left = { type: 'BinaryExpression', operator: 'instanceof', left, right }
1912
+ } else {
1913
+ break
1914
+ }
1915
+ }
1916
+
1917
+ return left
1918
+ }
1919
+
1920
+ parseConcatenation(lang) {
1921
+ let left = this.parseAdditive(lang)
1922
+
1923
+ while (this.match(TokenType.DOT) || this.matchValue('concatener') || this.matchValue('concatenation')) {
1924
+ const right = this.parseAdditive(lang)
1925
+ left = { type: 'BinaryExpression', operator: '.', left, right }
1926
+ }
1927
+
1928
+ return left
1929
+ }
1930
+
1931
+ parseAdditive(lang) {
1932
+ let left = this.parseMultiplicative(lang)
1933
+
1934
+ while (true) {
1935
+ if (this.match(TokenType.PLUS) || this.matchValue('plus') || this.matchValue('addition')) {
1936
+ const right = this.parseMultiplicative(lang)
1937
+ left = { type: 'BinaryExpression', operator: '+', left, right }
1938
+ } else if (this.match(TokenType.MINUS) || this.matchValue('moins') || this.matchValue('soustraction')) {
1939
+ const right = this.parseMultiplicative(lang)
1940
+ left = { type: 'BinaryExpression', operator: '-', left, right }
1941
+ } else {
1942
+ break
1943
+ }
1944
+ }
1945
+
1946
+ return left
1947
+ }
1948
+
1949
+ parseMultiplicative(lang) {
1950
+ let left = this.parseUnary(lang)
1951
+
1952
+ while (true) {
1953
+ if (this.match(TokenType.STAR) || this.matchValue('fois') || this.matchValue('multiplication')) {
1954
+ const right = this.parseUnary(lang)
1955
+ left = { type: 'BinaryExpression', operator: '*', left, right }
1956
+ } else if (this.match(TokenType.SLASH) || this.matchValue('divise') || this.matchValue('division')) {
1957
+ const right = this.parseUnary(lang)
1958
+ left = { type: 'BinaryExpression', operator: '/', left, right }
1959
+ } else if (this.match(TokenType.PERCENT) || this.matchValue('modulo')) {
1960
+ const right = this.parseUnary(lang)
1961
+ left = { type: 'BinaryExpression', operator: '%', left, right }
1962
+ } else if (this.match(TokenType.DOUBLE_STAR) || this.matchValue('puissance') || this.matchValue('**')) {
1963
+ const right = this.parseUnary(lang)
1964
+ left = { type: 'BinaryExpression', operator: '**', left, right }
1965
+ } else {
1966
+ break
1967
+ }
1968
+ }
1969
+
1970
+ return left
1971
+ }
1972
+
1973
+ parseUnary(lang) {
1974
+ if (this.match(TokenType.BANG) || this.matchValue('non') || this.matchValue('pas')) {
1975
+ const operand = this.parseUnary(lang)
1976
+ return { type: 'UnaryExpression', operator: '!', operand }
1977
+ }
1978
+
1979
+ if (this.match(TokenType.MINUS)) {
1980
+ const operand = this.parseUnary(lang)
1981
+ return { type: 'UnaryExpression', operator: '-', operand }
1982
+ }
1983
+
1984
+ if (this.match(TokenType.PLUS)) {
1985
+ const operand = this.parseUnary(lang)
1986
+ return { type: 'UnaryExpression', operator: '+', operand }
1987
+ }
1988
+
1989
+ if (this.match(TokenType.AT)) {
1990
+ const operand = this.parseUnary(lang)
1991
+ return { type: 'UnaryExpression', operator: '@', operand }
1992
+ }
1993
+
1994
+ if (this.match(TokenType.TILDE)) {
1995
+ const operand = this.parseUnary(lang)
1996
+ return { type: 'UnaryExpression', operator: '~', operand }
1997
+ }
1998
+
1999
+ if (this.match(TokenType.DOUBLE_PLUS)) {
2000
+ const operand = this.parseUnary(lang)
2001
+ return { type: 'UpdateExpression', operator: '++', prefix: true, operand }
2002
+ }
2003
+
2004
+ if (this.match(TokenType.DOUBLE_MINUS)) {
2005
+ const operand = this.parseUnary(lang)
2006
+ return { type: 'UpdateExpression', operator: '--', prefix: true, operand }
2007
+ }
2008
+
2009
+ if (this.matchValue('nouveau') || this.matchValue('nouvelle') || this.matchValue('new')) {
2010
+ return this.parseNew(lang)
2011
+ }
2012
+
2013
+ if (this.matchValue('clone') || this.matchValue('cloner')) {
2014
+ const operand = this.parseUnary(lang)
2015
+ return { type: 'CloneExpression', operand }
2016
+ }
2017
+
2018
+ if (this.matchValue('rendement') || this.matchValue('yield')) {
2019
+ let delegate = false
2020
+ if (this.matchValue('de') || this.matchValue('from')) {
2021
+ delegate = true
2022
+ }
2023
+ const argument = this.parseExpression(lang)
2024
+ return { type: 'YieldExpression', argument, delegate }
2025
+ }
2026
+
2027
+ const castTypes = ['int', 'integer', 'entier', 'float', 'double', 'flottant', 'string', 'chaine', 'bool', 'boolean', 'booleen', 'array', 'tableau', 'object', 'objet', 'unset']
2028
+ const val = this.current() ? this.normalizeAccents(String(this.current().value).toLowerCase()) : ''
2029
+ if (this.check(TokenType.LPAREN)) {
2030
+ const next = this.peek(1)
2031
+ if (next && castTypes.includes(this.normalizeAccents(String(next.value).toLowerCase()))) {
2032
+ this.advance()
2033
+ const castType = this.translateType(this.advance().value)
2034
+ this.match(TokenType.RPAREN)
2035
+ const operand = this.parseUnary(lang)
2036
+ return { type: 'CastExpression', castType, operand }
2037
+ }
2038
+ }
2039
+
2040
+ return this.parsePostfix(lang)
2041
+ }
2042
+
2043
+ parseNew(lang) {
2044
+ let className
2045
+
2046
+ const val = this.current() ? this.normalizeAccents(String(this.current().value).toLowerCase()) : ''
2047
+ if (val === 'classe' || val === 'class') {
2048
+ this.advance()
2049
+
2050
+ let superClass = null
2051
+ const extendsVal = this.current() ? this.normalizeAccents(String(this.current().value).toLowerCase()) : ''
2052
+ if (['etendre', 'etend', 'extends'].includes(extendsVal)) {
2053
+ this.advance()
2054
+ superClass = this.parseNamespacePath(lang)
2055
+ }
2056
+
2057
+ const interfaces = []
2058
+ const implementsVal = this.current() ? this.normalizeAccents(String(this.current().value).toLowerCase()) : ''
2059
+ if (['implementer', 'implemente', 'implements'].includes(implementsVal)) {
2060
+ this.advance()
2061
+ do {
2062
+ interfaces.push(this.parseNamespacePath(lang))
2063
+ } while (this.match(TokenType.COMMA))
2064
+ }
2065
+
2066
+ let args = []
2067
+ if (this.check(TokenType.LPAREN)) {
2068
+ this.advance()
2069
+ while (!this.isAtEnd() && !this.check(TokenType.RPAREN)) {
2070
+ args.push(this.parseExpression(lang))
2071
+ this.match(TokenType.COMMA)
2072
+ }
2073
+ this.match(TokenType.RPAREN)
2074
+ }
2075
+
2076
+ this.skipNewlines()
2077
+ const body = this.parseClassBody(lang)
2078
+
2079
+ return {
2080
+ type: 'NewExpression',
2081
+ callee: {
2082
+ type: 'AnonymousClass',
2083
+ superClass: superClass,
2084
+ interfaces: interfaces,
2085
+ body: body
2086
+ },
2087
+ arguments: args
2088
+ }
2089
+ }
2090
+
2091
+ className = this.parseNamespacePath(lang)
2092
+
2093
+ let args = []
2094
+ if (this.check(TokenType.LPAREN)) {
2095
+ this.advance()
2096
+ while (!this.isAtEnd() && !this.check(TokenType.RPAREN)) {
2097
+ this.skipNewlines()
2098
+ if (this.check(TokenType.RPAREN)) break
2099
+ args.push(this.parseExpression(lang))
2100
+ this.match(TokenType.COMMA)
2101
+ this.skipNewlines()
2102
+ }
2103
+ this.match(TokenType.RPAREN)
2104
+ }
2105
+
2106
+ return {
2107
+ type: 'NewExpression',
2108
+ callee: { type: 'Identifier', name: className },
2109
+ arguments: args
2110
+ }
2111
+ }
2112
+
2113
+ parsePostfix(lang) {
2114
+ let expr = this.parseCall(lang)
2115
+
2116
+ if (this.match(TokenType.DOUBLE_PLUS)) {
2117
+ return { type: 'UpdateExpression', operator: '++', prefix: false, operand: expr }
2118
+ }
2119
+
2120
+ if (this.match(TokenType.DOUBLE_MINUS)) {
2121
+ return { type: 'UpdateExpression', operator: '--', prefix: false, operand: expr }
2122
+ }
2123
+
2124
+ return expr
2125
+ }
2126
+
2127
+ parseCall(lang) {
2128
+ let expr = this.parsePrimary(lang)
2129
+
2130
+ while (true) {
2131
+ if (this.match(TokenType.LPAREN)) {
2132
+ const args = []
2133
+ while (!this.isAtEnd() && !this.check(TokenType.RPAREN)) {
2134
+ this.skipNewlines()
2135
+ if (this.check(TokenType.RPAREN)) break
2136
+
2137
+ if (this.match(TokenType.SPREAD) || this.matchValue('...')) {
2138
+ const arg = this.parseExpression(lang)
2139
+ args.push({ type: 'SpreadElement', argument: arg })
2140
+ } else {
2141
+ let argName = null
2142
+ if (this.current() && this.current().type === TokenType.IDENTIFIER) {
2143
+ const next = this.peek(1)
2144
+ if (next && next.type === TokenType.COLON) {
2145
+ argName = this.advance().value
2146
+ this.advance()
2147
+ }
2148
+ }
2149
+ const arg = this.parseExpression(lang)
2150
+ if (argName) {
2151
+ args.push({ type: 'NamedArgument', name: argName, value: arg })
2152
+ } else {
2153
+ args.push(arg)
2154
+ }
2155
+ }
2156
+ this.match(TokenType.COMMA)
2157
+ this.skipNewlines()
2158
+ }
2159
+ this.match(TokenType.RPAREN)
2160
+ expr = { type: 'CallExpression', callee: expr, arguments: args }
2161
+ } else if (this.match(TokenType.ARROW) || this.matchValue('->')) {
2162
+ const property = this.current()
2163
+ if (property && property.type === TokenType.IDENTIFIER) {
2164
+ this.advance()
2165
+ expr = { type: 'MemberExpression', object: expr, property: { type: 'Identifier', name: property.value }, operator: '->' }
2166
+ } else if (this.match(TokenType.LBRACE)) {
2167
+ const propExpr = this.parseExpression(lang)
2168
+ this.match(TokenType.RBRACE)
2169
+ expr = { type: 'MemberExpression', object: expr, property: propExpr, computed: true, operator: '->' }
2170
+ }
2171
+ } else if (this.match(TokenType.NULLSAFE_ARROW) || this.matchValue('?->')) {
2172
+ const property = this.current()
2173
+ if (property && property.type === TokenType.IDENTIFIER) {
2174
+ this.advance()
2175
+ expr = { type: 'MemberExpression', object: expr, property: { type: 'Identifier', name: property.value }, operator: '?->', nullsafe: true }
2176
+ }
2177
+ } else if (this.match(TokenType.DOUBLE_COLON) || this.matchValue('::')) {
2178
+ const member = this.current()
2179
+ if (member && member.type === TokenType.IDENTIFIER) {
2180
+ this.advance()
2181
+ expr = { type: 'StaticMemberExpression', class: expr, member: { type: 'Identifier', name: member.value } }
2182
+ } else if (member && member.value === 'class') {
2183
+ this.advance()
2184
+ expr = { type: 'ClassNameExpression', class: expr }
2185
+ }
2186
+ } else if (this.match(TokenType.LBRACKET)) {
2187
+ const index = this.check(TokenType.RBRACKET) ? null : this.parseExpression(lang)
2188
+ this.match(TokenType.RBRACKET)
2189
+ expr = { type: 'IndexExpression', object: expr, index }
2190
+ } else {
2191
+ break
2192
+ }
2193
+ }
2194
+
2195
+ return expr
2196
+ }
2197
+
2198
+ parsePrimary(lang) {
2199
+ const token = this.current()
2200
+
2201
+ if (!token) {
2202
+ return { type: 'Literal', value: null }
2203
+ }
2204
+
2205
+ if (token.type === TokenType.NUMBER || token.type === TokenType.INTEGER || token.type === TokenType.FLOAT) {
2206
+ this.advance()
2207
+ return { type: 'Literal', value: parseFloat(token.value) }
2208
+ }
2209
+
2210
+ if (token.type === TokenType.STRING) {
2211
+ this.advance()
2212
+ let value = token.value
2213
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
2214
+ value = value.slice(1, -1)
2215
+ }
2216
+ const isDouble = token.value.startsWith('"')
2217
+ return { type: 'StringLiteral', value, doubleQuoted: isDouble }
2218
+ }
2219
+
2220
+ if (token.type === TokenType.HEREDOC || token.type === TokenType.NOWDOC) {
2221
+ this.advance()
2222
+ return { type: 'HeredocLiteral', value: token.value }
2223
+ }
2224
+
2225
+ if (token.type === TokenType.IDENTIFIER) {
2226
+ const value = token.value
2227
+ const valueLower = this.normalizeAccents(value.toLowerCase())
2228
+
2229
+ if (valueLower === 'vrai' || valueLower === 'true') {
2230
+ this.advance()
2231
+ return { type: 'Literal', value: true }
2232
+ }
2233
+
2234
+ if (valueLower === 'faux' || valueLower === 'false') {
2235
+ this.advance()
2236
+ return { type: 'Literal', value: false }
2237
+ }
2238
+
2239
+ if (valueLower === 'nul' || valueLower === 'null' || valueLower === 'rien') {
2240
+ this.advance()
2241
+ return { type: 'Literal', value: null }
2242
+ }
2243
+
2244
+ if (valueLower === 'ceci' || valueLower === 'this') {
2245
+ this.advance()
2246
+ return { type: 'ThisExpression' }
2247
+ }
2248
+
2249
+ if (valueLower === 'self') {
2250
+ this.advance()
2251
+ return { type: 'Identifier', name: 'self' }
2252
+ }
2253
+
2254
+ if (valueLower === 'parent') {
2255
+ this.advance()
2256
+ return { type: 'Identifier', name: 'parent' }
2257
+ }
2258
+
2259
+ if (valueLower === 'static' || valueLower === 'statique') {
2260
+ this.advance()
2261
+ return { type: 'Identifier', name: 'static' }
2262
+ }
2263
+
2264
+ if (['fonction', 'function'].includes(valueLower)) {
2265
+ return this.parseFunctionExpression(lang)
2266
+ }
2267
+
2268
+ if (valueLower === 'fn') {
2269
+ return this.parseArrowFunction(lang)
2270
+ }
2271
+
2272
+ if (valueLower === 'match') {
2273
+ return this.parseMatchExpression(lang)
2274
+ }
2275
+
2276
+ if (value.startsWith('$')) {
2277
+ this.advance()
2278
+ return { type: 'Variable', name: value.substring(1) }
2279
+ }
2280
+
2281
+ const translated = this.translatePHP(value)
2282
+ this.advance()
2283
+ return { type: 'Identifier', name: translated || value }
2284
+ }
2285
+
2286
+ if (token.type === TokenType.VARIABLE) {
2287
+ this.advance()
2288
+ let name = token.value
2289
+ if (name.startsWith('$')) name = name.substring(1)
2290
+ return { type: 'Variable', name }
2291
+ }
2292
+
2293
+ if (token.type === TokenType.LBRACKET) {
2294
+ return this.parseArrayLiteral(lang)
2295
+ }
2296
+
2297
+ if (token.type === TokenType.LPAREN) {
2298
+ this.advance()
2299
+ const expr = this.parseExpression(lang)
2300
+ this.match(TokenType.RPAREN)
2301
+ expr.parenthesized = true
2302
+ return expr
2303
+ }
2304
+
2305
+ this.advance()
2306
+ return { type: 'Literal', value: null }
2307
+ }
2308
+
2309
+ parseFunctionExpression(lang) {
2310
+ this.advance()
2311
+
2312
+ let isReference = false
2313
+ if (this.match(TokenType.AMPERSAND)) {
2314
+ isReference = true
2315
+ }
2316
+
2317
+ const params = this.parseParameters(lang)
2318
+
2319
+ let uses = []
2320
+ const useVal = this.current() ? this.normalizeAccents(String(this.current().value).toLowerCase()) : ''
2321
+ if (useVal === 'utiliser' || useVal === 'use') {
2322
+ this.advance()
2323
+ uses = this.parseClosureUse(lang)
2324
+ }
2325
+
2326
+ let returnType = null
2327
+ if (this.match(TokenType.COLON)) {
2328
+ let nullable = false
2329
+ if (this.match(TokenType.QUESTION)) {
2330
+ nullable = true
2331
+ }
2332
+ returnType = this.parseTypeHint(lang)
2333
+ if (returnType && nullable) {
2334
+ returnType.nullable = true
2335
+ }
2336
+ }
2337
+
2338
+ this.skipNewlines()
2339
+ const body = this.parseBlock(lang)
2340
+
2341
+ return {
2342
+ type: 'FunctionExpression',
2343
+ params: params,
2344
+ uses: uses,
2345
+ returnType: returnType,
2346
+ body: body,
2347
+ reference: isReference
2348
+ }
2349
+ }
2350
+
2351
+ parseArrowFunction(lang) {
2352
+ this.advance()
2353
+
2354
+ const params = this.parseParameters(lang)
2355
+
2356
+ let returnType = null
2357
+ if (this.match(TokenType.COLON)) {
2358
+ returnType = this.parseTypeHint(lang)
2359
+ }
2360
+
2361
+ this.match(TokenType.DOUBLE_ARROW) || this.matchValue('=>')
2362
+
2363
+ const body = this.parseExpression(lang)
2364
+
2365
+ return {
2366
+ type: 'ArrowFunctionExpression',
2367
+ params: params,
2368
+ returnType: returnType,
2369
+ body: body
2370
+ }
2371
+ }
2372
+
2373
+ parseArrayLiteral(lang) {
2374
+ this.match(TokenType.LBRACKET)
2375
+ const elements = []
2376
+
2377
+ while (!this.isAtEnd() && !this.check(TokenType.RBRACKET)) {
2378
+ this.skipNewlines()
2379
+ if (this.check(TokenType.RBRACKET)) break
2380
+
2381
+ if (this.match(TokenType.SPREAD) || this.matchValue('...')) {
2382
+ const argument = this.parseExpression(lang)
2383
+ elements.push({ type: 'SpreadElement', argument })
2384
+ } else {
2385
+ const first = this.parseExpression(lang)
2386
+
2387
+ if (this.match(TokenType.DOUBLE_ARROW) || this.matchValue('=>')) {
2388
+ const value = this.parseExpression(lang)
2389
+ elements.push({ type: 'ArrayElement', key: first, value })
2390
+ } else {
2391
+ elements.push({ type: 'ArrayElement', key: null, value: first })
2392
+ }
2393
+ }
2394
+
2395
+ this.match(TokenType.COMMA)
2396
+ this.skipNewlines()
2397
+ }
2398
+
2399
+ this.match(TokenType.RBRACKET)
2400
+ return { type: 'ArrayExpression', elements }
2401
+ }
2402
+ }
2403
+
2404
+ module.exports = { EtherParserPHP }