Skip to content

Commit d7107c8

Browse files
authored
Merge pull request #22 from contriboss/feat/eval_gemfile
2 parents cec37be + a489eb9 commit d7107c8

File tree

3 files changed

+245
-16
lines changed

3 files changed

+245
-16
lines changed

CHANGELOG.md

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,21 @@
11
# Changelog
22

3-
## [0.10.0](https://github.com/contriboss/gemfile-go/compare/v0.9.0...v0.10.0) (2026-02-08)
4-
3+
## Unreleased
54

65
### Features
76

8-
* ✨ support single group and platform values ([89c9ea4](https://github.com/contriboss/gemfile-go/commit/89c9ea44c90cf7bf5cbeedb3969e5647fcf2c598))
9-
* ✨ support single group and platform values ([c4ab169](https://github.com/contriboss/gemfile-go/commit/c4ab1694953ad0e24b2affe21557993bf13a8c59))
10-
11-
12-
### Bug Fixes
13-
14-
* 🐛 release workflow: Use proper `/v2/` in path to `golangci-lint` in `magefile.go` ([daa2e69](https://github.com/contriboss/gemfile-go/commit/daa2e6961e2c2d3272218f47449c58b2e275c81d))
15-
* 🐛 release workflow: Use proper `/v2/` in path to `golangci-lint`… ([e85f354](https://github.com/contriboss/gemfile-go/commit/e85f3549e3d2f625c9060e666a37dab377fceaba))
16-
17-
## [Unreleased]
7+
* ✨ support `eval_gemfile` macro for modular Gemfiles
188

9+
## [0.10.0](https://github.com/contriboss/gemfile-go/compare/v0.9.0...v0.10.0) (2026-02-08)
1910

2011
### Features
2112

22-
* support single group and platform values (e.g., `group: :test` or `platform: :mri`)
13+
* support single group and platform values (e.g., `group: :test` or `platform: :mri`) ([c4ab169](https://github.com/contriboss/gemfile-go/commit/c4ab1694953ad0e24b2affe21557993bf13a8c59))
2314

2415
### Bug Fixes
2516

2617
* `release` workflow: Use proper `/v2/` in path to `golangci-lint` in `magefile.go`
2718

28-
2919
## [0.9.0](https://github.com/contriboss/gemfile-go/compare/v0.8.0...v0.9.0) (2026-02-08)
3020

3121

gemfile/eval_gemfile_test.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package gemfile
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
)
8+
9+
const (
10+
gemRspec = `gem "rspec"`
11+
rspec = "rspec"
12+
)
13+
14+
func TestParseEvalGemfile(t *testing.T) {
15+
tmpDir := t.TempDir()
16+
17+
// Create main Gemfile
18+
mainGemfileContent := `
19+
source "https://rubygems.org"
20+
gem "rails"
21+
eval_gemfile "modular/rspec.gemfile"
22+
`
23+
mainGemfilePath := filepath.Join(tmpDir, "Gemfile")
24+
if err := os.WriteFile(mainGemfilePath, []byte(mainGemfileContent), 0600); err != nil {
25+
t.Fatalf("Failed to write main Gemfile: %v", err)
26+
}
27+
28+
// Create modular directory
29+
modularDir := filepath.Join(tmpDir, "modular")
30+
if err := os.Mkdir(modularDir, 0700); err != nil {
31+
t.Fatalf("Failed to create modular directory: %v", err)
32+
}
33+
34+
// Create rspec.gemfile
35+
rspecGemfileContent := "\n" + gemRspec + `
36+
eval_gemfile "nested.gemfile"
37+
`
38+
rspecGemfilePath := filepath.Join(modularDir, "rspec.gemfile")
39+
if err := os.WriteFile(rspecGemfilePath, []byte(rspecGemfileContent), 0600); err != nil {
40+
t.Fatalf("Failed to write rspec Gemfile: %v", err)
41+
}
42+
43+
// Create nested.gemfile
44+
nestedGemfileContent := `
45+
gem "rspec-core"
46+
`
47+
nestedGemfilePath := filepath.Join(modularDir, "nested.gemfile")
48+
if err := os.WriteFile(nestedGemfilePath, []byte(nestedGemfileContent), 0600); err != nil {
49+
t.Fatalf("Failed to write nested Gemfile: %v", err)
50+
}
51+
52+
parser := NewGemfileParser(mainGemfilePath)
53+
parsed, err := parser.Parse()
54+
if err != nil {
55+
t.Fatalf("Failed to parse Gemfile: %v", err)
56+
}
57+
58+
expectedGems := []string{"rails", rspec, "rspec-core"}
59+
for _, expected := range expectedGems {
60+
found := false
61+
for _, gem := range parsed.Dependencies {
62+
if gem.Name == expected {
63+
found = true
64+
break
65+
}
66+
}
67+
if !found {
68+
t.Errorf("Expected gem %s not found", expected)
69+
}
70+
}
71+
72+
if len(parsed.Dependencies) != 3 {
73+
t.Errorf("Expected 3 gems, got %d", len(parsed.Dependencies))
74+
}
75+
}
76+
77+
func TestParseEvalGemfileAbsolute(t *testing.T) {
78+
tmpDir := t.TempDir()
79+
80+
// Create modular directory
81+
modularDir := filepath.Join(tmpDir, "modular")
82+
if err := os.Mkdir(modularDir, 0700); err != nil {
83+
t.Fatalf("Failed to create modular directory: %v", err)
84+
}
85+
86+
// Create rspec.gemfile
87+
rspecGemfileContent := gemRspec
88+
rspecGemfilePath := filepath.Join(modularDir, "rspec.gemfile")
89+
if err := os.WriteFile(rspecGemfilePath, []byte(rspecGemfileContent), 0600); err != nil {
90+
t.Fatalf("Failed to write rspec Gemfile: %v", err)
91+
}
92+
93+
// Create main Gemfile with absolute path
94+
mainGemfileContent := `eval_gemfile "` + rspecGemfilePath + `"`
95+
mainGemfilePath := filepath.Join(tmpDir, "Gemfile")
96+
if err := os.WriteFile(mainGemfilePath, []byte(mainGemfileContent), 0600); err != nil {
97+
t.Fatalf("Failed to write main Gemfile: %v", err)
98+
}
99+
100+
parser := NewGemfileParser(mainGemfilePath)
101+
parsed, err := parser.Parse()
102+
if err != nil {
103+
t.Fatalf("Failed to parse Gemfile: %v", err)
104+
}
105+
106+
if len(parsed.Dependencies) != 1 || parsed.Dependencies[0].Name != rspec {
107+
t.Errorf("Expected gem rspec, got %v", parsed.Dependencies)
108+
}
109+
}
110+
111+
func TestParseEvalGemfileWithGroups(t *testing.T) {
112+
tmpDir := t.TempDir()
113+
114+
// Create modular.gemfile
115+
modularContent := gemRspec
116+
modularPath := filepath.Join(tmpDir, "modular.gemfile")
117+
if err := os.WriteFile(modularPath, []byte(modularContent), 0600); err != nil {
118+
t.Fatalf("Failed to write modular Gemfile: %v", err)
119+
}
120+
121+
// Create main Gemfile with groups
122+
mainGemfileContent := `
123+
group :test do
124+
eval_gemfile "modular.gemfile"
125+
end
126+
`
127+
mainGemfilePath := filepath.Join(tmpDir, "Gemfile")
128+
if err := os.WriteFile(mainGemfilePath, []byte(mainGemfileContent), 0600); err != nil {
129+
t.Fatalf("Failed to write main Gemfile: %v", err)
130+
}
131+
132+
parser := NewGemfileParser(mainGemfilePath)
133+
parsed, err := parser.Parse()
134+
if err != nil {
135+
t.Fatalf("Failed to parse Gemfile: %v", err)
136+
}
137+
138+
if len(parsed.Dependencies) != 1 {
139+
t.Fatalf("Expected 1 gem, got %d", len(parsed.Dependencies))
140+
}
141+
142+
gem := parsed.Dependencies[0]
143+
if gem.Name != rspec {
144+
t.Errorf("Expected gem rspec, got %s", gem.Name)
145+
}
146+
147+
foundTestGroup := false
148+
for _, g := range gem.Groups {
149+
if g == "test" {
150+
foundTestGroup = true
151+
break
152+
}
153+
}
154+
if !foundTestGroup {
155+
t.Errorf("Expected gem to be in 'test' group, got %v", gem.Groups)
156+
}
157+
}

gemfile/parser.go

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ func (p *GemfileParser) Parse() (*ParsedGemfile, error) {
101101
// (gemspec integration needs more work)
102102
useTreeSitter := tsErr == nil &&
103103
(len(gemfile.Dependencies) > 0 || gemfile.RubyVersion != "") &&
104-
len(gemfile.Gemspecs) == 0
104+
len(gemfile.Gemspecs) == 0 &&
105+
!strings.Contains(p.content, "eval_gemfile")
105106

106107
if useTreeSitter {
107108
return gemfile, nil
@@ -245,7 +246,7 @@ func (p *GemfileParser) parseContent() (*ParsedGemfile, error) {
245246
expandedLine := p.expandVariables(line, variables)
246247

247248
// Parse different types of lines
248-
if err := p.parseLine(expandedLine, &currentGroups, &currentSource, &blockDepth, result); err != nil {
249+
if err := p.parseLine(expandedLine, &currentGroups, &currentSource, &blockDepth, result, variables); err != nil {
249250
return nil, fmt.Errorf("line %d: %w", lineNum, err)
250251
}
251252
}
@@ -288,9 +289,15 @@ func (p *GemfileParser) parseLine(
288289
currentSource **Source,
289290
blockDepth *int,
290291
result *ParsedGemfile,
292+
variables map[string]string,
291293
) error {
292294
line = strings.TrimSpace(line)
293295

296+
// Parse eval_gemfile
297+
if strings.HasPrefix(line, "eval_gemfile") {
298+
return p.handleEvalGemfile(line, currentGroups, currentSource, blockDepth, result, variables)
299+
}
300+
294301
// Parse source declarations
295302
if strings.HasPrefix(line, "source ") {
296303
source, isBlock, err := p.parseSource(line)
@@ -805,3 +812,78 @@ func (p *GemfileParser) handleGemspecDirective(line string, result *ParsedGemfil
805812
}
806813
return nil
807814
}
815+
816+
// handleEvalGemfile handles the eval_gemfile macro
817+
func (p *GemfileParser) handleEvalGemfile(
818+
line string,
819+
currentGroups *[]string,
820+
currentSource **Source,
821+
blockDepth *int,
822+
result *ParsedGemfile,
823+
variables map[string]string,
824+
) error {
825+
re := regexp.MustCompile(`eval_gemfile\s*\(?\s*['"]([^'"]+)['"]\s*\)?`)
826+
matches := re.FindStringSubmatch(line)
827+
if len(matches) < 2 {
828+
return fmt.Errorf("invalid eval_gemfile line: %s", line)
829+
}
830+
831+
includePath := matches[1]
832+
if !filepath.IsAbs(includePath) {
833+
dir := filepath.Dir(p.filepath)
834+
includePath = filepath.Join(dir, includePath)
835+
}
836+
837+
content, err := os.ReadFile(includePath)
838+
if err != nil {
839+
return fmt.Errorf("failed to read included Gemfile %s: %w", includePath, err)
840+
}
841+
842+
// Save current state
843+
oldFilepath := p.filepath
844+
oldContent := p.content
845+
846+
// Update state for recursive parsing
847+
p.filepath = includePath
848+
p.content = string(content)
849+
850+
// Parse the included content
851+
scanner := bufio.NewScanner(strings.NewReader(p.content))
852+
lineNum := 0
853+
condHandler := newConditionalHandler(p)
854+
855+
for scanner.Scan() {
856+
lineNum++
857+
line := strings.TrimSpace(scanner.Text())
858+
859+
if line == "" || strings.HasPrefix(line, "#") {
860+
continue
861+
}
862+
863+
if handled, skip := condHandler.handleLine(line); handled && skip {
864+
continue
865+
}
866+
867+
if !condHandler.shouldProcess() {
868+
condHandler.handleInactiveLine(line)
869+
continue
870+
}
871+
872+
if varName, varValue := p.parseVariable(line); varName != "" {
873+
variables[varName] = varValue
874+
continue
875+
}
876+
877+
expandedLine := p.expandVariables(line, variables)
878+
879+
if err := p.parseLine(expandedLine, currentGroups, currentSource, blockDepth, result, variables); err != nil {
880+
return fmt.Errorf("in %s line %d: %w", includePath, lineNum, err)
881+
}
882+
}
883+
884+
// Restore state
885+
p.filepath = oldFilepath
886+
p.content = oldContent
887+
888+
return nil
889+
}

0 commit comments

Comments
 (0)