Skip to content

Commit 166e57a

Browse files
fix(extgen): correctly handle const blocks to declare iota constants
1 parent 75ccccf commit 166e57a

File tree

6 files changed

+289
-32
lines changed

6 files changed

+289
-32
lines changed

docs/extensions.md

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -406,10 +406,10 @@ 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

415415
#### Class Constants
@@ -429,13 +429,11 @@ const STATUS_INACTIVE = 0
429429
const ROLE_ADMIN = "admin"
430430

431431
//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
432+
const (
433+
STATE_PENDING = iota
434+
STATE_PROCESSING
435+
STATE_COMPLETED
436+
)
439437
```
440438

441439
Class constants are accessible using the class name scope in PHP:

docs/fr/extensions.md

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -402,10 +402,10 @@ 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

411411
#### Constantes de Classe
@@ -425,13 +425,11 @@ const STATUS_INACTIVE = 0
425425
const ROLE_ADMIN = "admin"
426426

427427
//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
428+
const (
429+
STATE_PENDING = iota
430+
STATE_PROCESSING
431+
STATE_COMPLETED
432+
)
435433
```
436434

437435
Les constantes de classe sont accessibles en utilisant la portée du nom de classe en 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

internal/extgen/integration_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,7 @@ func TestConstants(t *testing.T) {
480480
[]string{
481481
"TEST_MAX_RETRIES", "TEST_API_VERSION", "TEST_ENABLED", "TEST_PI",
482482
"STATUS_PENDING", "STATUS_PROCESSING", "STATUS_COMPLETED",
483+
"ONE", "TWO",
483484
},
484485
)
485486
require.NoError(t, err, "all constants, functions, and classes should be accessible from PHP")

0 commit comments

Comments
 (0)