• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hmerritt / reactenv / 21911899119

11 Feb 2026 03:44PM UTC coverage: 26.739%. First build
21911899119

push

github

web-flow
Merge pull request #4 from hmerritt/dev

Refactor occurrence finder algorithm to fix edge cases

## Changes
- Refactor occurrence finder algorithm to fix edge cases
- `FindOccurrences` returns an error, and it will fail `run` command
- Test `reactenv` logic
- GitHub actions testing

58 of 68 new or added lines in 2 files covered. (85.29%)

173 of 647 relevant lines covered (26.74%)

0.31 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.65
/reactenv/reactenv.go
1
package reactenv
2

3
import (
4
        "bytes"
5
        "fmt"
6
        "io/fs"
7
        "os"
8
        "path"
9
        "regexp"
10
        "strings"
11

12
        "github.com/hmerritt/reactenv/ui"
13
)
14

15
const (
16
        REACTENV_PREFIX = "__reactenv"
17
)
18

19
type Reactenv struct {
20
        UI *ui.Ui
21

22
        // Path of directory to scan
23
        Dir string
24

25
        // Total file count - that have matches - within specified `Dir`
26
        FilesMatchTotal int
27
        // Files with occurrences (not every matched file will have an occurrence, so this may be less than `FilesMatchTotal`)
28
        Files []*fs.DirEntry
29

30
        // Total individual occurrences count
31
        OccurrencesTotal int
32
        // Each file occurrence count + keys
33
        OccurrencesByFile []*FileOccurrences
34
        // Map of all unique environment variable keys
35
        OccurrenceKeys OccurrenceKeys
36
        // Map of all environment variable key values (keys will be replaced with these values)
37
        OccurrenceKeysReplacement OccurrenceKeysReplacement
38
}
39

40
type Occurrence = struct {
41
        Key      string
42
        StartEnd []int
43
}
44
type OccurrenceKeys = map[string]bool
45
type OccurrenceKeysReplacement = map[string]string
46
type FileOccurrences = struct {
47
        Occurrences []Occurrence
48
}
49

50
func NewReactenv(ui *ui.Ui) *Reactenv {
1✔
51
        return &Reactenv{
1✔
52
                UI:                        ui,
1✔
53
                Dir:                       "",
1✔
54
                Files:                     make([]*fs.DirEntry, 0),
1✔
55
                OccurrencesTotal:          0,
1✔
56
                OccurrencesByFile:         make([]*FileOccurrences, 0),
1✔
57
                OccurrenceKeys:            make(OccurrenceKeys),
1✔
58
                OccurrenceKeysReplacement: make(OccurrenceKeysReplacement),
1✔
59
        }
1✔
60
}
1✔
61

62
// Populates `Reactenv.Files` with all files that match `fileMatchExpression`
63
func (r *Reactenv) FindFiles(dir string, fileMatchExpression string) error {
1✔
64
        r.Dir = dir
1✔
65
        r.Files = make([]*fs.DirEntry, 0)
1✔
66
        files, err := os.ReadDir(r.Dir)
1✔
67

1✔
68
        if err != nil {
2✔
69
                return err
1✔
70
        }
1✔
71

72
        fileMatcher, err := regexp.Compile(fileMatchExpression)
1✔
73

1✔
74
        if err != nil {
2✔
75
                return err
1✔
76
        }
1✔
77

78
        for _, file := range files {
2✔
79
                if fileMatcher.MatchString(file.Name()) && !file.IsDir() {
2✔
80
                        fileEntry := file
1✔
81
                        r.Files = append(r.Files, &fileEntry)
1✔
82
                }
1✔
83
        }
84

85
        r.FilesMatchTotal = len(r.Files)
1✔
86

1✔
87
        return nil
1✔
88
}
89

90
// Run a callback for each File
91
func (r *Reactenv) FilesWalk(fileCb func(fileIndex int, file fs.DirEntry, filePath string) error) error {
1✔
92
        for fileIndex, file := range r.Files {
2✔
93
                filePath := path.Join(r.Dir, (*file).Name())
1✔
94
                err := fileCb(fileIndex, *file, filePath)
1✔
95
                if err != nil {
2✔
96
                        return err
1✔
97
                }
1✔
98
        }
99

100
        return nil
1✔
101
}
102

103
// Run a callback for each File, passing in the file contents
104
func (r *Reactenv) FilesWalkContents(fileCb func(fileIndex int, file fs.DirEntry, filePath string, fileContents []byte) error) error {
1✔
105
        for fileIndex, file := range r.Files {
2✔
106
                filePath := path.Join(r.Dir, (*file).Name())
1✔
107
                fileContents, err := os.ReadFile(filePath)
1✔
108

1✔
109
                if err != nil {
2✔
110
                        r.UI.Error(fmt.Sprintf("Error when reading file '%s'.\n", (*file).Name()))
1✔
111
                        r.UI.Error(fmt.Sprintf("%v", err))
1✔
112
                        os.Exit(1)
1✔
113
                }
1✔
114

115
                err = fileCb(fileIndex, *file, filePath, fileContents)
1✔
116

1✔
117
                if err != nil {
2✔
118
                        return err
1✔
119
                }
1✔
120
        }
121

122
        return nil
1✔
123
}
124

125
// Strictly locates all instances of valid reactenv variables and returns their byte
126
// start and end indices.
127
//
128
// This implementation allows for side-by-side occurrences, which a regex pattern
129
// match would not (without an unsupported lookahead assertion).
130
func FindAllOccurrenceBytePositions(data []byte, prefix []byte) [][]int {
1✔
131
        // Guard against infinite loop allocation caused by empty prefixes.
1✔
132
        if len(prefix) == 0 {
2✔
133
                return nil
1✔
134
        }
1✔
135

136
        var indices [][]int
1✔
137

1✔
138
        // Establish absolute boundaries by finding all prefix locations
1✔
139
        var prefixStarts []int
1✔
140
        offset := 0
1✔
141
        for {
2✔
142
                idx := bytes.Index(data[offset:], prefix)
1✔
143
                if idx == -1 {
2✔
144
                        break
1✔
145
                }
146
                absoluteIdx := offset + idx
1✔
147
                prefixStarts = append(prefixStarts, absoluteIdx)
1✔
148
                offset = absoluteIdx + len(prefix)
1✔
149
        }
150

151
        // Iterate through boundaries and enforce positional grammar
152
        for i, start := range prefixStarts {
2✔
153
                current := start + len(prefix)
1✔
154

1✔
155
                limit := len(data)
1✔
156
                if i+1 < len(prefixStarts) {
2✔
157
                        limit = prefixStarts[i+1]
1✔
158
                }
1✔
159

160
                // Validate the initial character (cannot be a numeral).
161
                if current < limit {
2✔
162
                        firstByte := data[current]
1✔
163
                        isValidStart := (firstByte >= 'a' && firstByte <= 'z') ||
1✔
164
                                (firstByte >= 'A' && firstByte <= 'Z') ||
1✔
165
                                firstByte == '_' || firstByte == '$'
1✔
166

1✔
167
                        if !isValidStart {
2✔
168
                                // Abort: The character following the dot is invalid
1✔
169
                                continue
1✔
170
                        }
171
                        current++
1✔
172
                } else {
1✔
173
                        // Abort: The string terminates immediately after the dot
1✔
174
                        continue
1✔
175
                }
176

177
                // Scan forward for all subsequent valid identifier bytes
178
                for current < limit {
2✔
179
                        b := data[current]
1✔
180
                        isValid := (b >= 'a' && b <= 'z') ||
1✔
181
                                (b >= 'A' && b <= 'Z') ||
1✔
182
                                (b >= '0' && b <= '9') ||
1✔
183
                                b == '_' || b == '$'
1✔
184

1✔
185
                        if !isValid {
2✔
186
                                break
1✔
187
                        }
188
                        current++
1✔
189
                }
190

191
                indices = append(indices, []int{start, current})
1✔
192
        }
193

194
        return indices
1✔
195
}
196

197
// Walks every file and populates `Reactenv.Occurrences*` fields.
198
func (r *Reactenv) FindOccurrences() error {
1✔
199
        // Reset occurrence fields
1✔
200
        r.OccurrencesTotal = 0
1✔
201
        r.OccurrencesByFile = make([]*FileOccurrences, 0)
1✔
202
        r.OccurrenceKeys = make(OccurrenceKeys)
1✔
203
        r.OccurrenceKeysReplacement = make(OccurrenceKeysReplacement)
1✔
204

1✔
205
        // Prep for removing files with no occurrences
1✔
206
        newFiles := make([]*fs.DirEntry, 0, len(r.Files))
1✔
207
        newOccurrencesByFile := make([]*FileOccurrences, 0)
1✔
208
        fileIndexesToRemove := make(map[int]int, 0)
1✔
209

1✔
210
        err := r.FilesWalkContents(func(fileIndex int, file fs.DirEntry, filePath string, fileContents []byte) error {
2✔
211
                prefix := fmt.Appendf([]byte(""), "%s.", REACTENV_PREFIX)
1✔
212
                fileOccurrences := FindAllOccurrenceBytePositions(fileContents, prefix)
1✔
213

1✔
214
                fileOccurrencesToStore := make([]Occurrence, 0, len(fileOccurrences))
1✔
215
                r.OccurrencesTotal += len(fileOccurrences)
1✔
216
                r.OccurrencesByFile = append(r.OccurrencesByFile, &FileOccurrences{
1✔
217
                        Occurrences: fileOccurrencesToStore,
1✔
218
                })
1✔
219

1✔
220
                for _, occurrence := range fileOccurrences {
2✔
221
                        occurrenceText := string(fileContents[occurrence[0]:occurrence[1]])
1✔
222
                        envName := strings.Replace(occurrenceText, "__reactenv.", "", 1)
1✔
223
                        envValue, envExists := os.LookupEnv(envName)
1✔
224

1✔
225
                        r.OccurrencesByFile[fileIndex].Occurrences = append(r.OccurrencesByFile[fileIndex].Occurrences, Occurrence{
1✔
226
                                Key:      envName,
1✔
227
                                StartEnd: occurrence,
1✔
228
                        })
1✔
229

1✔
230
                        r.OccurrenceKeys[envName] = true
1✔
231

1✔
232
                        if envExists {
2✔
233
                                r.OccurrenceKeysReplacement[envName] = envValue
1✔
234
                        }
1✔
235
                }
236

237
                if len(fileOccurrences) == 0 {
2✔
238
                        fileIndexesToRemove[fileIndex] = fileIndex
1✔
239
                }
1✔
240

241
                return nil
1✔
242
        })
243

244
        if err != nil {
1✔
NEW
245
                return err
×
NEW
246
        }
×
247

248
        // Remove files with no occurrences
249
        if len(fileIndexesToRemove) > 0 {
2✔
250
                for fileIndex, file := range r.Files {
2✔
251
                        if _, ok := fileIndexesToRemove[fileIndex]; !ok {
2✔
252
                                newFiles = append(newFiles, file)
1✔
253
                                newOccurrencesByFile = append(newOccurrencesByFile, r.OccurrencesByFile[fileIndex])
1✔
254
                        }
1✔
255
                }
256

257
                r.Files = newFiles
1✔
258
                r.OccurrencesByFile = newOccurrencesByFile
1✔
259
        }
260

261
        return nil
1✔
262
}
263

264
func (r *Reactenv) ReplaceOccurrences() {
1✔
265
        r.FilesWalkContents(func(fileIndex int, file fs.DirEntry, filePath string, fileContents []byte) error {
2✔
266
                fileContentsNew := make([]byte, 0, len(fileContents))
1✔
267

1✔
268
                lastIndex := 0
1✔
269
                for _, occurrence := range r.OccurrencesByFile[fileIndex].Occurrences {
2✔
270
                        start, end := occurrence.StartEnd[0], occurrence.StartEnd[1]
1✔
271
                        envValue := r.OccurrenceKeysReplacement[occurrence.Key]
1✔
272

1✔
273
                        fileContentsNew = append(fileContentsNew, fileContents[lastIndex:start]...)
1✔
274
                        fileContentsNew = append(fileContentsNew, envValue...)
1✔
275
                        lastIndex = end
1✔
276
                }
1✔
277
                fileContentsNew = append(fileContentsNew, fileContents[lastIndex:]...)
1✔
278

1✔
279
                if err := os.WriteFile(filePath, fileContentsNew, 0644); err != nil {
1✔
280
                        r.UI.Error(fmt.Sprintf("Error when writing to file '%s'.\n", filePath))
×
281
                        r.UI.Error(fmt.Sprintf("%v", err))
×
282
                        os.Exit(1)
×
283
                }
×
284

285
                return nil
1✔
286
        })
287
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc