diff --git a/docs/extensions.md b/docs/extensions.md index 30f434eb5..6959d482f 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -406,10 +406,10 @@ const MAX_CONNECTIONS = 100 const API_VERSION = "1.2.3" //export_php:const -const STATUS_OK = iota - -//export_php:const -const STATUS_ERROR = iota +const ( + STATUS_OK = iota + STATUS_ERROR +) ``` #### Class Constants @@ -429,13 +429,11 @@ const STATUS_INACTIVE = 0 const ROLE_ADMIN = "admin" //export_php:classconst Order -const STATE_PENDING = iota - -//export_php:classconst Order -const STATE_PROCESSING = iota - -//export_php:classconst Order -const STATE_COMPLETED = iota +const ( + STATE_PENDING = iota + STATE_PROCESSING + STATE_COMPLETED +) ``` Class constants are accessible using the class name scope in PHP: diff --git a/docs/fr/extensions.md b/docs/fr/extensions.md index caf50e9ca..05840b524 100644 --- a/docs/fr/extensions.md +++ b/docs/fr/extensions.md @@ -402,10 +402,10 @@ const MAX_CONNECTIONS = 100 const API_VERSION = "1.2.3" //export_php:const -const STATUS_OK = iota - -//export_php:const -const STATUS_ERROR = iota +const ( + STATUS_OK = iota + STATUS_ERROR +) ``` #### Constantes de Classe @@ -425,13 +425,11 @@ const STATUS_INACTIVE = 0 const ROLE_ADMIN = "admin" //export_php:classconst Order -const STATE_PENDING = iota - -//export_php:classconst Order -const STATE_PROCESSING = iota - -//export_php:classconst Order -const STATE_COMPLETED = iota +const ( + STATE_PENDING = iota + STATE_PROCESSING + STATE_COMPLETED +) ``` Les constantes de classe sont accessibles en utilisant la portée du nom de classe en PHP : diff --git a/internal/extgen/constparser.go b/internal/extgen/constparser.go index 2f304895d..86f80337c 100644 --- a/internal/extgen/constparser.go +++ b/internal/extgen/constparser.go @@ -34,6 +34,10 @@ func (cp *ConstantParser) parse(filename string) (constants []phpConstant, err e expectClassConstDecl := false currentClassName := "" currentConstantValue := 0 + inConstBlock := false + exportAllInBlock := false + lastConstValue := "" + lastConstWasIota := false for scanner.Scan() { lineNumber++ @@ -55,7 +59,26 @@ func (cp *ConstantParser) parse(filename string) (constants []phpConstant, err e continue } - if (expectConstDecl || expectClassConstDecl) && strings.HasPrefix(line, "const ") { + if strings.HasPrefix(line, "const (") { + inConstBlock = true + if expectConstDecl || expectClassConstDecl { + exportAllInBlock = true + } + continue + } + + if inConstBlock && line == ")" { + inConstBlock = false + exportAllInBlock = false + expectConstDecl = false + expectClassConstDecl = false + currentClassName = "" + lastConstValue = "" + lastConstWasIota = false + continue + } + + if (expectConstDecl || expectClassConstDecl) && strings.HasPrefix(line, "const ") && !inConstBlock { matches := constDeclRegex.FindStringSubmatch(line) if len(matches) == 3 { name := matches[1] @@ -72,10 +95,11 @@ func (cp *ConstantParser) parse(filename string) (constants []phpConstant, err e constant.PhpType = determineConstantType(value) if constant.IsIota { - // affect a default value because user didn't give one constant.Value = fmt.Sprintf("%d", currentConstantValue) constant.PhpType = phpInt currentConstantValue++ + lastConstWasIota = true + lastConstValue = constant.Value } constants = append(constants, constant) @@ -84,7 +108,65 @@ func (cp *ConstantParser) parse(filename string) (constants []phpConstant, err e } expectConstDecl = false expectClassConstDecl = false - } else if (expectConstDecl || expectClassConstDecl) && !strings.HasPrefix(line, "//") && line != "" { + } else if inConstBlock && (expectConstDecl || expectClassConstDecl || exportAllInBlock) { + constBlockDeclRegex := regexp.MustCompile(`^(\w+)\s*=\s*(.+)$`) + if matches := constBlockDeclRegex.FindStringSubmatch(line); len(matches) == 3 { + name := matches[1] + value := strings.TrimSpace(matches[2]) + + constant := phpConstant{ + Name: name, + Value: value, + IsIota: value == "iota", + lineNumber: lineNumber, + ClassName: currentClassName, + } + + constant.PhpType = determineConstantType(value) + + if constant.IsIota { + constant.Value = fmt.Sprintf("%d", currentConstantValue) + constant.PhpType = phpInt + currentConstantValue++ + lastConstWasIota = true + lastConstValue = constant.Value + } else { + lastConstWasIota = false + lastConstValue = value + } + + constants = append(constants, constant) + expectConstDecl = false + expectClassConstDecl = false + } else { + constNameRegex := regexp.MustCompile(`^(\w+)$`) + if matches := constNameRegex.FindStringSubmatch(line); len(matches) == 2 { + name := matches[1] + + constant := phpConstant{ + Name: name, + Value: "", + IsIota: lastConstWasIota, + lineNumber: lineNumber, + ClassName: currentClassName, + } + + if lastConstWasIota { + constant.Value = fmt.Sprintf("%d", currentConstantValue) + constant.PhpType = phpInt + currentConstantValue++ + lastConstValue = constant.Value + } else { + constant.Value = lastConstValue + constant.PhpType = determineConstantType(lastConstValue) + } + + constants = append(constants, constant) + expectConstDecl = false + expectClassConstDecl = false + } + } + } else if (expectConstDecl || expectClassConstDecl) && !strings.HasPrefix(line, "//") && line != "" && !inConstBlock { // we expected a const declaration but found something else, reset expectConstDecl = false expectClassConstDecl = false diff --git a/internal/extgen/constparser_test.go b/internal/extgen/constparser_test.go index 29f0e38fd..7c5ce89f2 100644 --- a/internal/extgen/constparser_test.go +++ b/internal/extgen/constparser_test.go @@ -221,7 +221,7 @@ func TestConstantParserIotaSequence(t *testing.T) { //export_php:const const FirstIota = iota -//export_php:const +//export_php:const const SecondIota = iota //export_php:const @@ -244,6 +244,179 @@ const ThirdIota = iota` } } +func TestConstantParserConstBlock(t *testing.T) { + input := `package main + +const ( + // export_php:const + STATUS_PENDING = iota + + // export_php:const + STATUS_PROCESSING + + // export_php:const + STATUS_COMPLETED +)` + + tmpDir := t.TempDir() + fileName := filepath.Join(tmpDir, "test.go") + require.NoError(t, os.WriteFile(fileName, []byte(input), 0644)) + + parser := &ConstantParser{} + constants, err := parser.parse(fileName) + assert.NoError(t, err, "parse() error") + + assert.Len(t, constants, 3, "Expected 3 constants") + + expectedNames := []string{"STATUS_PENDING", "STATUS_PROCESSING", "STATUS_COMPLETED"} + expectedValues := []string{"0", "1", "2"} + + for i, c := range constants { + assert.Equal(t, expectedNames[i], c.Name, "Expected constant %d name to be '%s'", i, expectedNames[i]) + assert.True(t, c.IsIota, "Expected constant %d to be iota", i) + assert.Equal(t, expectedValues[i], c.Value, "Expected constant %d value to be '%s'", i, expectedValues[i]) + assert.Equal(t, phpInt, c.PhpType, "Expected constant %d to be phpInt type", i) + } +} + +func TestConstantParserConstBlockWithBlockLevelDirective(t *testing.T) { + input := `package main + +// export_php:const +const ( + STATUS_PENDING = iota + STATUS_PROCESSING + STATUS_COMPLETED +)` + + tmpDir := t.TempDir() + fileName := filepath.Join(tmpDir, "test.go") + require.NoError(t, os.WriteFile(fileName, []byte(input), 0644)) + + parser := &ConstantParser{} + constants, err := parser.parse(fileName) + assert.NoError(t, err, "parse() error") + + assert.Len(t, constants, 3, "Expected 3 constants") + + expectedNames := []string{"STATUS_PENDING", "STATUS_PROCESSING", "STATUS_COMPLETED"} + expectedValues := []string{"0", "1", "2"} + + for i, c := range constants { + assert.Equal(t, expectedNames[i], c.Name, "Expected constant %d name to be '%s'", i, expectedNames[i]) + assert.True(t, c.IsIota, "Expected constant %d to be iota", i) + assert.Equal(t, expectedValues[i], c.Value, "Expected constant %d value to be '%s'", i, expectedValues[i]) + assert.Equal(t, phpInt, c.PhpType, "Expected constant %d to be phpInt type", i) + } +} + +func TestConstantParserMixedConstBlockAndIndividual(t *testing.T) { + input := `package main + +// export_php:const +const INDIVIDUAL = 42 + +const ( + // export_php:const + BLOCK_ONE = iota + + // export_php:const + BLOCK_TWO +) + +// export_php:const +const ANOTHER_INDIVIDUAL = "test"` + + tmpDir := t.TempDir() + fileName := filepath.Join(tmpDir, "test.go") + require.NoError(t, os.WriteFile(fileName, []byte(input), 0644)) + + parser := &ConstantParser{} + constants, err := parser.parse(fileName) + assert.NoError(t, err, "parse() error") + + assert.Len(t, constants, 4, "Expected 4 constants") + + assert.Equal(t, "INDIVIDUAL", constants[0].Name) + assert.Equal(t, "42", constants[0].Value) + assert.Equal(t, phpInt, constants[0].PhpType) + + assert.Equal(t, "BLOCK_ONE", constants[1].Name) + assert.Equal(t, "0", constants[1].Value) + assert.True(t, constants[1].IsIota) + + assert.Equal(t, "BLOCK_TWO", constants[2].Name) + assert.Equal(t, "1", constants[2].Value) + assert.True(t, constants[2].IsIota) + + assert.Equal(t, "ANOTHER_INDIVIDUAL", constants[3].Name) + assert.Equal(t, `"test"`, constants[3].Value) + assert.Equal(t, phpString, constants[3].PhpType) +} + +func TestConstantParserClassConstBlock(t *testing.T) { + input := `package main + +// export_php:classconst Config +const ( + MODE_DEBUG = 1 + MODE_PRODUCTION = 2 + MODE_TEST = 3 +)` + + tmpDir := t.TempDir() + fileName := filepath.Join(tmpDir, "test.go") + require.NoError(t, os.WriteFile(fileName, []byte(input), 0644)) + + parser := &ConstantParser{} + constants, err := parser.parse(fileName) + assert.NoError(t, err, "parse() error") + + assert.Len(t, constants, 3, "Expected 3 class constants") + + expectedNames := []string{"MODE_DEBUG", "MODE_PRODUCTION", "MODE_TEST"} + expectedValues := []string{"1", "2", "3"} + + for i, c := range constants { + assert.Equal(t, expectedNames[i], c.Name, "Expected constant %d name to be '%s'", i, expectedNames[i]) + assert.Equal(t, "Config", c.ClassName, "Expected constant %d to belong to Config class", i) + assert.Equal(t, expectedValues[i], c.Value, "Expected constant %d value to be '%s'", i, expectedValues[i]) + assert.Equal(t, phpInt, c.PhpType, "Expected constant %d to be phpInt type", i) + } +} + +func TestConstantParserClassConstBlockWithIota(t *testing.T) { + input := `package main + +// export_php:classconst Status +const ( + STATUS_PENDING = iota + STATUS_ACTIVE + STATUS_COMPLETED +)` + + tmpDir := t.TempDir() + fileName := filepath.Join(tmpDir, "test.go") + require.NoError(t, os.WriteFile(fileName, []byte(input), 0644)) + + parser := &ConstantParser{} + constants, err := parser.parse(fileName) + assert.NoError(t, err, "parse() error") + + assert.Len(t, constants, 3, "Expected 3 class constants") + + expectedNames := []string{"STATUS_PENDING", "STATUS_ACTIVE", "STATUS_COMPLETED"} + expectedValues := []string{"0", "1", "2"} + + for i, c := range constants { + assert.Equal(t, expectedNames[i], c.Name, "Expected constant %d name to be '%s'", i, expectedNames[i]) + assert.Equal(t, "Status", c.ClassName, "Expected constant %d to belong to Status class", i) + assert.True(t, c.IsIota, "Expected constant %d to be iota", i) + assert.Equal(t, expectedValues[i], c.Value, "Expected constant %d value to be '%s'", i, expectedValues[i]) + assert.Equal(t, phpInt, c.PhpType, "Expected constant %d to be phpInt type", i) + } +} + func TestConstantParserTypeDetection(t *testing.T) { tests := []struct { name string diff --git a/internal/extgen/integration_test.go b/internal/extgen/integration_test.go index 86723fc39..4c3539906 100644 --- a/internal/extgen/integration_test.go +++ b/internal/extgen/integration_test.go @@ -480,6 +480,7 @@ func TestConstants(t *testing.T) { []string{ "TEST_MAX_RETRIES", "TEST_API_VERSION", "TEST_ENABLED", "TEST_PI", "STATUS_PENDING", "STATUS_PROCESSING", "STATUS_COMPLETED", + "ONE", "TWO", }, ) require.NoError(t, err, "all constants, functions, and classes should be accessible from PHP") diff --git a/testdata/integration/constants.go b/testdata/integration/constants.go index 9bb6bab9a..b40c1a588 100644 --- a/testdata/integration/constants.go +++ b/testdata/integration/constants.go @@ -21,13 +21,18 @@ const TEST_ENABLED = true const TEST_PI = 3.14159 // export_php:const -const STATUS_PENDING = iota - -// export_php:const -const STATUS_PROCESSING = iota +const ( + STATUS_PENDING = iota + STATUS_PROCESSING + STATUS_COMPLETED +) -// export_php:const -const STATUS_COMPLETED = iota +const ( + // export_php:const + ONE = 1 + // export_php:const + TWO = 2 +) // export_php:class Config type ConfigStruct struct {