Skip to content

Commit f8785c8

Browse files
committed
[auto] Add php detector
1 parent 73fcc6f commit f8785c8

File tree

4 files changed

+268
-6
lines changed

4 files changed

+268
-6
lines changed

pkg/autodetect/autodetect.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@ func populateConfig(ctx context.Context, path string, config *devconfig.Config)
4141

4242
func detectors(path string) []detector.Detector {
4343
return []detector.Detector{
44-
&detector.PythonDetector{Root: path},
45-
&detector.PoetryDetector{Root: path},
4644
&detector.GoDetector{Root: path},
45+
&detector.PHPDetector{Root: path},
46+
&detector.PoetryDetector{Root: path},
47+
&detector.PythonDetector{Root: path},
4748
}
4849
}
4950

@@ -62,6 +63,13 @@ func relevantDetector(path string) (detector.Detector, error) {
6263
relevantScore := 0.0
6364
var mostRelevantDetector detector.Detector
6465
for _, detector := range detectors(path) {
66+
if d, ok := detector.(interface {
67+
Init() error
68+
}); ok {
69+
if err := d.Init(); err != nil {
70+
return nil, err
71+
}
72+
}
6573
score, err := detector.Relevance(path)
6674
if err != nil {
6775
return nil, err

pkg/autodetect/detector/go.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,8 @@ func (d *GoDetector) Packages(ctx context.Context) ([]string, error) {
3434

3535
// Parse the Go version from go.mod
3636
goVersion := parseGoVersion(string(goModContent))
37-
if goVersion != "" {
38-
return []string{"go@" + goVersion}, nil
39-
}
40-
return []string{"go@latest"}, nil
37+
goVersion = determineBestVersion(ctx, "go", goVersion)
38+
return []string{"go@" + goVersion}, nil
4139
}
4240

4341
func parseGoVersion(goModContent string) string {

pkg/autodetect/detector/php.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package detector
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
"regexp"
10+
"strings"
11+
)
12+
13+
type composerJSON struct {
14+
Require map[string]string `json:"require"`
15+
}
16+
17+
type PHPDetector struct {
18+
Root string
19+
composerJSON *composerJSON
20+
}
21+
22+
var _ Detector = &PHPDetector{}
23+
24+
func (d *PHPDetector) Init() error {
25+
composer, err := loadComposerJSON(d.Root)
26+
if err != nil && !os.IsNotExist(err) {
27+
return err
28+
}
29+
d.composerJSON = composer
30+
return nil
31+
}
32+
33+
func (d *PHPDetector) Relevance(path string) (float64, error) {
34+
if d.composerJSON == nil {
35+
return 1, nil
36+
}
37+
return 0, nil
38+
}
39+
40+
func (d *PHPDetector) Packages(ctx context.Context) ([]string, error) {
41+
version, err := d.phpVersion()
42+
if err != nil {
43+
return nil, err
44+
}
45+
return []string{fmt.Sprintf("php@%s", version)}, nil
46+
}
47+
48+
func (d *PHPDetector) phpVersion() (string, error) {
49+
require := d.composerJSON.Require
50+
51+
if require["php"] == "" {
52+
return "latest", nil
53+
}
54+
// Remove the caret (^) if present
55+
version := strings.TrimPrefix(require["php"], "^")
56+
57+
// Extract version in the format x, x.y, or x.y.z
58+
re := regexp.MustCompile(`^(\d+(\.\d+){0,2})`)
59+
match := re.FindString(version)
60+
if match == "" {
61+
return "latest", nil
62+
}
63+
64+
version = match
65+
66+
return version, nil
67+
}
68+
69+
func (d *PHPDetector) phpExtensions() ([]string, error) {
70+
extensions := []string{}
71+
for key := range d.composerJSON.Require {
72+
if strings.HasPrefix(key, "ext-") {
73+
extensions = append(extensions, "phpExtensions."+strings.TrimPrefix(key, "ext-"))
74+
}
75+
}
76+
77+
return extensions, nil
78+
}
79+
80+
func loadComposerJSON(root string) (*composerJSON, error) {
81+
composerPath := filepath.Join(root, "composer.json")
82+
composerData, err := os.ReadFile(composerPath)
83+
if err != nil {
84+
return nil, err
85+
}
86+
var composer composerJSON
87+
err = json.Unmarshal(composerData, &composer)
88+
if err != nil {
89+
return nil, err
90+
}
91+
return &composer, nil
92+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package detector
2+
3+
import (
4+
"context"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestPHPDetector_Relevance(t *testing.T) {
14+
tests := []struct {
15+
name string
16+
setup func(t *testing.T) string
17+
expected float64
18+
}{
19+
{
20+
name: "no composer.json",
21+
setup: func(t *testing.T) string {
22+
dir := t.TempDir()
23+
return dir
24+
},
25+
expected: 1,
26+
},
27+
{
28+
name: "with composer.json",
29+
setup: func(t *testing.T) string {
30+
dir := t.TempDir()
31+
err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte(`{
32+
"require": {
33+
"php": "^8.1"
34+
}
35+
}`), 0644)
36+
require.NoError(t, err)
37+
return dir
38+
},
39+
expected: 0,
40+
},
41+
}
42+
43+
for _, tt := range tests {
44+
t.Run(tt.name, func(t *testing.T) {
45+
dir := tt.setup(t)
46+
d := &PHPDetector{Root: dir}
47+
err := d.Init()
48+
require.NoError(t, err)
49+
50+
score, err := d.Relevance(dir)
51+
require.NoError(t, err)
52+
assert.Equal(t, tt.expected, score)
53+
})
54+
}
55+
}
56+
57+
func TestPHPDetector_Packages(t *testing.T) {
58+
tests := []struct {
59+
name string
60+
composerJSON string
61+
expectedPHP string
62+
expectedError bool
63+
}{
64+
{
65+
name: "no php version specified",
66+
composerJSON: `{
67+
"require": {}
68+
}`,
69+
expectedPHP: "php@latest",
70+
},
71+
{
72+
name: "specific php version",
73+
composerJSON: `{
74+
"require": {
75+
"php": "^8.1"
76+
}
77+
}`,
78+
expectedPHP: "[email protected]",
79+
},
80+
{
81+
name: "php version with patch",
82+
composerJSON: `{
83+
"require": {
84+
"php": "^8.1.2"
85+
}
86+
}`,
87+
expectedPHP: "[email protected]",
88+
},
89+
{
90+
name: "invalid composer.json",
91+
composerJSON: `invalid json`,
92+
expectedError: true,
93+
},
94+
}
95+
96+
for _, curTest := range tests {
97+
t.Run(curTest.name, func(t *testing.T) {
98+
dir := t.TempDir()
99+
if curTest.composerJSON != "" {
100+
err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte(curTest.composerJSON), 0644)
101+
require.NoError(t, err)
102+
}
103+
104+
d := &PHPDetector{Root: dir}
105+
err := d.Init()
106+
if curTest.expectedError {
107+
require.Error(t, err)
108+
return
109+
}
110+
require.NoError(t, err)
111+
112+
packages, err := d.Packages(context.Background())
113+
require.NoError(t, err)
114+
assert.Equal(t, []string{curTest.expectedPHP}, packages)
115+
})
116+
}
117+
}
118+
119+
func TestPHPDetector_PHPExtensions(t *testing.T) {
120+
tests := []struct {
121+
name string
122+
composerJSON string
123+
expectedExtensions []string
124+
}{
125+
{
126+
name: "no extensions",
127+
composerJSON: `{
128+
"require": {
129+
"php": "^8.1"
130+
}
131+
}`,
132+
expectedExtensions: []string{},
133+
},
134+
{
135+
name: "multiple extensions",
136+
composerJSON: `{
137+
"require": {
138+
"ext-mbstring": "*",
139+
"ext-imagick": "*"
140+
}
141+
}`,
142+
expectedExtensions: []string{
143+
"phpExtensions.mbstring",
144+
"phpExtensions.imagick",
145+
},
146+
},
147+
}
148+
149+
for _, tt := range tests {
150+
t.Run(tt.name, func(t *testing.T) {
151+
dir := t.TempDir()
152+
err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte(tt.composerJSON), 0644)
153+
require.NoError(t, err)
154+
155+
d := &PHPDetector{Root: dir}
156+
err = d.Init()
157+
require.NoError(t, err)
158+
159+
extensions, err := d.phpExtensions()
160+
require.NoError(t, err)
161+
assert.ElementsMatch(t, tt.expectedExtensions, extensions)
162+
})
163+
}
164+
}

0 commit comments

Comments
 (0)