Skip to content

Commit c6b2b02

Browse files
fix(extgen): correctly handle const blocks to declare iota constants (#2086)
While continuing the work on #2011, I realized that constant declarations have a problem when using `iota`. I mean, it technically works, but const *blocks* we not supported which means that setting all constants to `iota` as shown in the documentation was non-sensical, as `iota` resets every time outside of const blocks. So, this is between the bug fix and the feature. To me, it's a bug fix as the behavior wasn't the one intended when creating extgen.
1 parent ecad5ec commit c6b2b02

File tree

6 files changed

+301
-32
lines changed

6 files changed

+301
-32
lines changed

docs/extensions.md

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -406,12 +406,15 @@ const MAX_CONNECTIONS = 100
406406
const API_VERSION = "1.2.3"
407407

408408
//export_php:const
409-
const STATUS_OK = iota
410-
411-
//export_php:const
412-
const STATUS_ERROR = iota
409+
const (
410+
STATUS_OK = iota
411+
STATUS_ERROR
412+
)
413413
```
414414

415+
> [!NOTE]
416+
> PHP constants will take the name of the Go constant, thus using upper case letters is recommended.
417+
415418
#### Class Constants
416419

417420
Use the `//export_php:classconst ClassName` directive to create constants that belong to a specific PHP class:
@@ -429,15 +432,16 @@ const STATUS_INACTIVE = 0
429432
const ROLE_ADMIN = "admin"
430433

431434
//export_php:classconst Order
432-
const STATE_PENDING = iota
433-
434-
//export_php:classconst Order
435-
const STATE_PROCESSING = iota
436-
437-
//export_php:classconst Order
438-
const STATE_COMPLETED = iota
435+
const (
436+
STATE_PENDING = iota
437+
STATE_PROCESSING
438+
STATE_COMPLETED
439+
)
439440
```
440441

442+
> [!NOTE]
443+
> Just like global constants, the class constants will take the name of the Go constant.
444+
441445
Class constants are accessible using the class name scope in PHP:
442446

443447
```php

docs/fr/extensions.md

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -402,12 +402,15 @@ const MAX_CONNECTIONS = 100
402402
const API_VERSION = "1.2.3"
403403

404404
//export_php:const
405-
const STATUS_OK = iota
406-
407-
//export_php:const
408-
const STATUS_ERROR = iota
405+
const (
406+
STATUS_OK = iota
407+
STATUS_ERROR
408+
)
409409
```
410410

411+
> [!NOTE]
412+
> Les constantes PHP prennent le nom de la constante Go, d'où l'utilisation de majuscules pour les noms des constants en Go.
413+
411414
#### Constantes de Classe
412415

413416
Utilisez la directive `//export_php:classconst ClassName` pour créer des constantes qui appartiennent à une classe PHP spécifique :
@@ -425,15 +428,16 @@ const STATUS_INACTIVE = 0
425428
const ROLE_ADMIN = "admin"
426429

427430
//export_php:classconst Order
428-
const STATE_PENDING = iota
429-
430-
//export_php:classconst Order
431-
const STATE_PROCESSING = iota
432-
433-
//export_php:classconst Order
434-
const STATE_COMPLETED = iota
431+
const (
432+
STATE_PENDING = iota
433+
STATE_PROCESSING
434+
STATE_COMPLETED
435+
)
435436
```
436437

438+
> [!NOTE]
439+
> Comme les constantes globales, les constantes de classe prennent le nom de la constante Go.
440+
437441
Les constantes de classe sont accessibles en utilisant la portée du nom de classe en PHP :
438442

439443
```php

internal/extgen/constparser.go

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ func (cp *ConstantParser) parse(filename string) (constants []phpConstant, err e
3434
expectClassConstDecl := false
3535
currentClassName := ""
3636
currentConstantValue := 0
37+
inConstBlock := false
38+
exportAllInBlock := false
39+
lastConstValue := ""
40+
lastConstWasIota := false
3741

3842
for scanner.Scan() {
3943
lineNumber++
@@ -55,7 +59,26 @@ func (cp *ConstantParser) parse(filename string) (constants []phpConstant, err e
5559
continue
5660
}
5761

58-
if (expectConstDecl || expectClassConstDecl) && strings.HasPrefix(line, "const ") {
62+
if strings.HasPrefix(line, "const (") {
63+
inConstBlock = true
64+
if expectConstDecl || expectClassConstDecl {
65+
exportAllInBlock = true
66+
}
67+
continue
68+
}
69+
70+
if inConstBlock && line == ")" {
71+
inConstBlock = false
72+
exportAllInBlock = false
73+
expectConstDecl = false
74+
expectClassConstDecl = false
75+
currentClassName = ""
76+
lastConstValue = ""
77+
lastConstWasIota = false
78+
continue
79+
}
80+
81+
if (expectConstDecl || expectClassConstDecl) && strings.HasPrefix(line, "const ") && !inConstBlock {
5982
matches := constDeclRegex.FindStringSubmatch(line)
6083
if len(matches) == 3 {
6184
name := matches[1]
@@ -72,10 +95,11 @@ func (cp *ConstantParser) parse(filename string) (constants []phpConstant, err e
7295
constant.PhpType = determineConstantType(value)
7396

7497
if constant.IsIota {
75-
// affect a default value because user didn't give one
7698
constant.Value = fmt.Sprintf("%d", currentConstantValue)
7799
constant.PhpType = phpInt
78100
currentConstantValue++
101+
lastConstWasIota = true
102+
lastConstValue = constant.Value
79103
}
80104

81105
constants = append(constants, constant)
@@ -84,7 +108,65 @@ func (cp *ConstantParser) parse(filename string) (constants []phpConstant, err e
84108
}
85109
expectConstDecl = false
86110
expectClassConstDecl = false
87-
} else if (expectConstDecl || expectClassConstDecl) && !strings.HasPrefix(line, "//") && line != "" {
111+
} else if inConstBlock && (expectConstDecl || expectClassConstDecl || exportAllInBlock) {
112+
constBlockDeclRegex := regexp.MustCompile(`^(\w+)\s*=\s*(.+)$`)
113+
if matches := constBlockDeclRegex.FindStringSubmatch(line); len(matches) == 3 {
114+
name := matches[1]
115+
value := strings.TrimSpace(matches[2])
116+
117+
constant := phpConstant{
118+
Name: name,
119+
Value: value,
120+
IsIota: value == "iota",
121+
lineNumber: lineNumber,
122+
ClassName: currentClassName,
123+
}
124+
125+
constant.PhpType = determineConstantType(value)
126+
127+
if constant.IsIota {
128+
constant.Value = fmt.Sprintf("%d", currentConstantValue)
129+
constant.PhpType = phpInt
130+
currentConstantValue++
131+
lastConstWasIota = true
132+
lastConstValue = constant.Value
133+
} else {
134+
lastConstWasIota = false
135+
lastConstValue = value
136+
}
137+
138+
constants = append(constants, constant)
139+
expectConstDecl = false
140+
expectClassConstDecl = false
141+
} else {
142+
constNameRegex := regexp.MustCompile(`^(\w+)$`)
143+
if matches := constNameRegex.FindStringSubmatch(line); len(matches) == 2 {
144+
name := matches[1]
145+
146+
constant := phpConstant{
147+
Name: name,
148+
Value: "",
149+
IsIota: lastConstWasIota,
150+
lineNumber: lineNumber,
151+
ClassName: currentClassName,
152+
}
153+
154+
if lastConstWasIota {
155+
constant.Value = fmt.Sprintf("%d", currentConstantValue)
156+
constant.PhpType = phpInt
157+
currentConstantValue++
158+
lastConstValue = constant.Value
159+
} else {
160+
constant.Value = lastConstValue
161+
constant.PhpType = determineConstantType(lastConstValue)
162+
}
163+
164+
constants = append(constants, constant)
165+
expectConstDecl = false
166+
expectClassConstDecl = false
167+
}
168+
}
169+
} else if (expectConstDecl || expectClassConstDecl) && !strings.HasPrefix(line, "//") && line != "" && !inConstBlock {
88170
// we expected a const declaration but found something else, reset
89171
expectConstDecl = false
90172
expectClassConstDecl = false

internal/extgen/constparser_test.go

Lines changed: 174 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ func TestConstantParserIotaSequence(t *testing.T) {
221221
//export_php:const
222222
const FirstIota = iota
223223
224-
//export_php:const
224+
//export_php:const
225225
const SecondIota = iota
226226
227227
//export_php:const
@@ -244,6 +244,179 @@ const ThirdIota = iota`
244244
}
245245
}
246246

247+
func TestConstantParserConstBlock(t *testing.T) {
248+
input := `package main
249+
250+
const (
251+
// export_php:const
252+
STATUS_PENDING = iota
253+
254+
// export_php:const
255+
STATUS_PROCESSING
256+
257+
// export_php:const
258+
STATUS_COMPLETED
259+
)`
260+
261+
tmpDir := t.TempDir()
262+
fileName := filepath.Join(tmpDir, "test.go")
263+
require.NoError(t, os.WriteFile(fileName, []byte(input), 0644))
264+
265+
parser := &ConstantParser{}
266+
constants, err := parser.parse(fileName)
267+
assert.NoError(t, err, "parse() error")
268+
269+
assert.Len(t, constants, 3, "Expected 3 constants")
270+
271+
expectedNames := []string{"STATUS_PENDING", "STATUS_PROCESSING", "STATUS_COMPLETED"}
272+
expectedValues := []string{"0", "1", "2"}
273+
274+
for i, c := range constants {
275+
assert.Equal(t, expectedNames[i], c.Name, "Expected constant %d name to be '%s'", i, expectedNames[i])
276+
assert.True(t, c.IsIota, "Expected constant %d to be iota", i)
277+
assert.Equal(t, expectedValues[i], c.Value, "Expected constant %d value to be '%s'", i, expectedValues[i])
278+
assert.Equal(t, phpInt, c.PhpType, "Expected constant %d to be phpInt type", i)
279+
}
280+
}
281+
282+
func TestConstantParserConstBlockWithBlockLevelDirective(t *testing.T) {
283+
input := `package main
284+
285+
// export_php:const
286+
const (
287+
STATUS_PENDING = iota
288+
STATUS_PROCESSING
289+
STATUS_COMPLETED
290+
)`
291+
292+
tmpDir := t.TempDir()
293+
fileName := filepath.Join(tmpDir, "test.go")
294+
require.NoError(t, os.WriteFile(fileName, []byte(input), 0644))
295+
296+
parser := &ConstantParser{}
297+
constants, err := parser.parse(fileName)
298+
assert.NoError(t, err, "parse() error")
299+
300+
assert.Len(t, constants, 3, "Expected 3 constants")
301+
302+
expectedNames := []string{"STATUS_PENDING", "STATUS_PROCESSING", "STATUS_COMPLETED"}
303+
expectedValues := []string{"0", "1", "2"}
304+
305+
for i, c := range constants {
306+
assert.Equal(t, expectedNames[i], c.Name, "Expected constant %d name to be '%s'", i, expectedNames[i])
307+
assert.True(t, c.IsIota, "Expected constant %d to be iota", i)
308+
assert.Equal(t, expectedValues[i], c.Value, "Expected constant %d value to be '%s'", i, expectedValues[i])
309+
assert.Equal(t, phpInt, c.PhpType, "Expected constant %d to be phpInt type", i)
310+
}
311+
}
312+
313+
func TestConstantParserMixedConstBlockAndIndividual(t *testing.T) {
314+
input := `package main
315+
316+
// export_php:const
317+
const INDIVIDUAL = 42
318+
319+
const (
320+
// export_php:const
321+
BLOCK_ONE = iota
322+
323+
// export_php:const
324+
BLOCK_TWO
325+
)
326+
327+
// export_php:const
328+
const ANOTHER_INDIVIDUAL = "test"`
329+
330+
tmpDir := t.TempDir()
331+
fileName := filepath.Join(tmpDir, "test.go")
332+
require.NoError(t, os.WriteFile(fileName, []byte(input), 0644))
333+
334+
parser := &ConstantParser{}
335+
constants, err := parser.parse(fileName)
336+
assert.NoError(t, err, "parse() error")
337+
338+
assert.Len(t, constants, 4, "Expected 4 constants")
339+
340+
assert.Equal(t, "INDIVIDUAL", constants[0].Name)
341+
assert.Equal(t, "42", constants[0].Value)
342+
assert.Equal(t, phpInt, constants[0].PhpType)
343+
344+
assert.Equal(t, "BLOCK_ONE", constants[1].Name)
345+
assert.Equal(t, "0", constants[1].Value)
346+
assert.True(t, constants[1].IsIota)
347+
348+
assert.Equal(t, "BLOCK_TWO", constants[2].Name)
349+
assert.Equal(t, "1", constants[2].Value)
350+
assert.True(t, constants[2].IsIota)
351+
352+
assert.Equal(t, "ANOTHER_INDIVIDUAL", constants[3].Name)
353+
assert.Equal(t, `"test"`, constants[3].Value)
354+
assert.Equal(t, phpString, constants[3].PhpType)
355+
}
356+
357+
func TestConstantParserClassConstBlock(t *testing.T) {
358+
input := `package main
359+
360+
// export_php:classconst Config
361+
const (
362+
MODE_DEBUG = 1
363+
MODE_PRODUCTION = 2
364+
MODE_TEST = 3
365+
)`
366+
367+
tmpDir := t.TempDir()
368+
fileName := filepath.Join(tmpDir, "test.go")
369+
require.NoError(t, os.WriteFile(fileName, []byte(input), 0644))
370+
371+
parser := &ConstantParser{}
372+
constants, err := parser.parse(fileName)
373+
assert.NoError(t, err, "parse() error")
374+
375+
assert.Len(t, constants, 3, "Expected 3 class constants")
376+
377+
expectedNames := []string{"MODE_DEBUG", "MODE_PRODUCTION", "MODE_TEST"}
378+
expectedValues := []string{"1", "2", "3"}
379+
380+
for i, c := range constants {
381+
assert.Equal(t, expectedNames[i], c.Name, "Expected constant %d name to be '%s'", i, expectedNames[i])
382+
assert.Equal(t, "Config", c.ClassName, "Expected constant %d to belong to Config class", i)
383+
assert.Equal(t, expectedValues[i], c.Value, "Expected constant %d value to be '%s'", i, expectedValues[i])
384+
assert.Equal(t, phpInt, c.PhpType, "Expected constant %d to be phpInt type", i)
385+
}
386+
}
387+
388+
func TestConstantParserClassConstBlockWithIota(t *testing.T) {
389+
input := `package main
390+
391+
// export_php:classconst Status
392+
const (
393+
STATUS_PENDING = iota
394+
STATUS_ACTIVE
395+
STATUS_COMPLETED
396+
)`
397+
398+
tmpDir := t.TempDir()
399+
fileName := filepath.Join(tmpDir, "test.go")
400+
require.NoError(t, os.WriteFile(fileName, []byte(input), 0644))
401+
402+
parser := &ConstantParser{}
403+
constants, err := parser.parse(fileName)
404+
assert.NoError(t, err, "parse() error")
405+
406+
assert.Len(t, constants, 3, "Expected 3 class constants")
407+
408+
expectedNames := []string{"STATUS_PENDING", "STATUS_ACTIVE", "STATUS_COMPLETED"}
409+
expectedValues := []string{"0", "1", "2"}
410+
411+
for i, c := range constants {
412+
assert.Equal(t, expectedNames[i], c.Name, "Expected constant %d name to be '%s'", i, expectedNames[i])
413+
assert.Equal(t, "Status", c.ClassName, "Expected constant %d to belong to Status class", i)
414+
assert.True(t, c.IsIota, "Expected constant %d to be iota", i)
415+
assert.Equal(t, expectedValues[i], c.Value, "Expected constant %d value to be '%s'", i, expectedValues[i])
416+
assert.Equal(t, phpInt, c.PhpType, "Expected constant %d to be phpInt type", i)
417+
}
418+
}
419+
247420
func TestConstantParserTypeDetection(t *testing.T) {
248421
tests := []struct {
249422
name string

0 commit comments

Comments
 (0)