refactor(terraform): replace HCL parser with minimal version scanner

The hashicorp/hcl/v2 + zclconf/go-cty stack existed to extract a single
attribute: required_version from a top-level terraform block. Replace it
with a small scanner that tracks just enough HCL structure (comments,
strings, interpolation, heredocs, block nesting) to locate that
attribute, and fails closed to the state-file fallback on anything it
cannot make sense of - the same path a full parser failure took.

Shrinks the stripped linux/amd64 binary by 1.80 MB (16.93 MB -> 15.13 MB).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qiyvpiy5jR2tyzwZ3zUki5
This commit is contained in:
Claude
2026-07-31 07:40:24 +02:00
committed by Jan De Dobbeleer
parent 43b4cb3e3b
commit 2b64976e6b
5 changed files with 447 additions and 43 deletions
-8
View File
@@ -25,7 +25,6 @@ require (
require (
github.com/ConradIrwin/font v0.2.1
github.com/gookit/goutil v0.8.0
github.com/hashicorp/hcl/v2 v2.24.0
github.com/invopop/jsonschema v0.14.0
github.com/lucasb-eyer/go-colorful v1.4.0
github.com/pelletier/go-toml/v2 v2.4.3
@@ -54,23 +53,16 @@ require (
require (
dario.cat/mergo v1.0.2 // indirect
dmitri.shuralyov.com/font/woff2 v0.0.0-20180220214647-957792cbbdab // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.2 // indirect
github.com/dsnet/compress v0.0.1 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/pb33f/ordered-map/v2 v2.3.1 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zclconf/go-cty v1.17.0 // indirect
go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/tools v0.47.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
-18
View File
@@ -12,16 +12,12 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/alecthomas/assert v1.0.0 h1:3XmGh/PSuLzDbK3W2gUbRXwgW5lqPkuqvRgeQ30FI5o=
github.com/alecthomas/assert v1.0.0/go.mod h1:va/d2JC+M7F6s+80kl/R3G7FUiW6JzUO+hPhLyJ36ZY=
github.com/alecthomas/colour v0.1.0 h1:nOE9rJm6dsZ66RGWYSFrXw461ZIt9A6+nHgL7FRrDUk=
github.com/alecthomas/colour v0.1.0/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
@@ -40,8 +36,6 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -52,8 +46,6 @@ github.com/gookit/color v1.6.1 h1:KoTnDxJPRgrL0SoX0f8rCFg2zI0t4E3GZZBMo2nN8LU=
github.com/gookit/color v1.6.1/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
github.com/gookit/goutil v0.8.0 h1:efZWxfesXw8+5tQfTfRMSIC6A0ax527/H+A/aIiaSrw=
github.com/gookit/goutil v0.8.0/go.mod h1:vJS9HXctYTCLtCsZot5L5xF+O1oR17cDYO9R0HxBmnU=
github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -77,8 +69,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY=
@@ -130,10 +120,6 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0=
github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
@@ -145,8 +131,6 @@ golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -160,8 +144,6 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+4 -17
View File
@@ -7,8 +7,6 @@ import (
"slices"
"strings"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclparse"
"github.com/jandedobbeleer/oh-my-posh/src/segments/options"
)
@@ -27,12 +25,8 @@ func (tf *Terraform) Template() string {
return " {{ .WorkspaceName }}{{ if .Version }} {{ .Version }}{{ end }} "
}
type TerraFormConfig struct {
Terraform *TerraformBlock `hcl:"terraform,block"`
}
type TerraformBlock struct {
Version *string `hcl:"required_version" json:"terraform_version"`
Version *string `json:"terraform_version"`
}
func (tf *Terraform) Enabled() bool {
@@ -127,20 +121,13 @@ func (tf *Terraform) setVersionFromTfFiles() error {
continue
}
parser := hclparse.NewParser()
content := tf.env.FileContent(file)
hclFile, diags := parser.ParseHCL([]byte(content), file)
if diags != nil {
version, ok := extractRequiredVersion(content)
if !ok {
continue
}
var config TerraFormConfig
diags = gohcl.DecodeBody(hclFile.Body, nil, &config)
if diags != nil || config.Terraform == nil {
continue
}
tf.TerraformBlock = *config.Terraform
tf.Version = &version
return nil
}
return errors.New("no valid terraform files found")
+250
View File
@@ -0,0 +1,250 @@
package segments
import "strings"
// extractRequiredVersion scans HCL source for a top-level terraform block and
// returns the value of its required_version attribute. It is not an HCL
// parser: it only tracks the structure needed to locate that one attribute -
// comments, strings (including template interpolation), heredocs and block
// nesting. Whenever the source doesn't match what it expects, it reports
// false and the segment falls back to the state file, the same path a full
// parser failure took before.
func extractRequiredVersion(src string) (string, bool) {
s := &hclScanner{src: src, terraformDepth: -1}
for !s.eof() {
s.skipTrivia()
if s.eof() {
break
}
switch c := s.src[s.pos]; {
case c == '"':
s.skipString()
case c == '<' && s.peek() == '<':
s.skipHeredoc()
case c == '{':
s.depth++
s.pos++
case c == '}':
s.depth--
if s.depth < s.terraformDepth {
s.terraformDepth = -1
}
s.pos++
case isHCLIdentStart(c):
ident := s.readIdent()
if version, ok := s.handleIdent(ident); ok {
return version, true
}
default:
s.pos++
}
}
return "", false
}
type hclScanner struct {
src string
pos int
depth int
terraformDepth int
}
// handleIdent inspects an identifier just read at the current depth. A
// top-level terraform identifier followed by { opens the block we're after;
// required_version directly inside it yields the result.
func (s *hclScanner) handleIdent(ident string) (string, bool) {
if s.depth == 0 && ident == "terraform" {
s.skipTrivia()
if !s.eof() && s.src[s.pos] == '{' {
s.depth++
s.terraformDepth = s.depth
s.pos++
}
return "", false
}
if ident != "required_version" || s.terraformDepth == -1 || s.depth != s.terraformDepth {
return "", false
}
s.skipTrivia()
if s.eof() || s.src[s.pos] != '=' {
return "", false
}
s.pos++
s.skipTrivia()
if s.eof() || s.src[s.pos] != '"' {
return "", false
}
return s.readString()
}
func (s *hclScanner) eof() bool {
return s.pos >= len(s.src)
}
func (s *hclScanner) peek() byte {
if s.pos+1 >= len(s.src) {
return 0
}
return s.src[s.pos+1]
}
// skipTrivia advances past whitespace and comments (#, // and /* */).
func (s *hclScanner) skipTrivia() {
for !s.eof() {
switch c := s.src[s.pos]; {
case c == ' ' || c == '\t' || c == '\r' || c == '\n':
s.pos++
case c == '#' || (c == '/' && s.peek() == '/'):
for !s.eof() && s.src[s.pos] != '\n' {
s.pos++
}
case c == '/' && s.peek() == '*':
s.pos += 2
for !s.eof() && (s.src[s.pos] != '*' || s.peek() != '/') {
s.pos++
}
s.pos += 2
default:
return
}
}
}
// skipString advances past a quoted string, honoring escapes and ${ }
// template interpolation, which may itself contain strings and braces.
func (s *hclScanner) skipString() {
_, _ = s.readString()
}
// readString consumes a quoted string starting at the opening quote and
// returns its content with common escapes resolved. Interpolation is copied
// verbatim; required_version must be a static string, so a value containing
// ${ } simply won't match a version constraint downstream.
func (s *hclScanner) readString() (string, bool) {
var sb strings.Builder
s.pos++ // opening quote
for !s.eof() {
switch c := s.src[s.pos]; {
case c == '"':
s.pos++
return sb.String(), true
case c == '\\':
escaped, ok := hclEscape(s.peek())
if ok {
sb.WriteByte(escaped)
}
if !ok {
sb.WriteByte(c)
sb.WriteByte(s.peek())
}
s.pos += 2
case c == '$' && s.peek() == '{':
start := s.pos
s.skipInterpolation()
sb.WriteString(s.src[start:s.pos])
default:
sb.WriteByte(c)
s.pos++
}
}
return "", false
}
// skipInterpolation advances past a ${ } template expression, tracking brace
// nesting and skipping over nested strings so their braces don't miscount.
func (s *hclScanner) skipInterpolation() {
s.pos += 2
braces := 1
for !s.eof() && braces > 0 {
switch s.src[s.pos] {
case '{':
braces++
s.pos++
case '}':
braces--
s.pos++
case '"':
s.skipString()
default:
s.pos++
}
}
}
// skipHeredoc advances past a << or <<- heredoc, which runs until a line
// containing only the delimiter label.
func (s *hclScanner) skipHeredoc() {
s.pos += 2
if !s.eof() && s.src[s.pos] == '-' {
s.pos++
}
start := s.pos
for !s.eof() && s.src[s.pos] != '\n' {
s.pos++
}
label := strings.TrimSpace(s.src[start:s.pos])
if label == "" {
return
}
for !s.eof() {
s.pos++ // the newline
lineStart := s.pos
for !s.eof() && s.src[s.pos] != '\n' {
s.pos++
}
if strings.TrimSpace(s.src[lineStart:s.pos]) == label {
return
}
}
}
func (s *hclScanner) readIdent() string {
start := s.pos
for !s.eof() && isHCLIdentPart(s.src[s.pos]) {
s.pos++
}
return s.src[start:s.pos]
}
func hclEscape(c byte) (byte, bool) {
switch c {
case 'n':
return '\n', true
case 't':
return '\t', true
case 'r':
return '\r', true
case '"':
return '"', true
case '\\':
return '\\', true
default:
return 0, false
}
}
func isHCLIdentStart(c byte) bool {
return c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}
func isHCLIdentPart(c byte) bool {
return isHCLIdentStart(c) || c == '-' || (c >= '0' && c <= '9')
}
+193
View File
@@ -0,0 +1,193 @@
package segments
import (
"testing"
"github.com/alecthomas/assert"
)
func TestExtractRequiredVersion(t *testing.T) {
cases := []struct {
Case string
Source string
Expected string
OK bool
}{
{
Case: "simple block",
Source: "terraform {\n required_version = \">= 1.0.10\"\n}\n",
Expected: ">= 1.0.10",
OK: true,
},
{
Case: "nested required_providers before the attribute",
Source: `terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
required_version = ">= 1.3.0, < 2.0.0"
}`,
Expected: ">= 1.3.0, < 2.0.0",
OK: true,
},
{
Case: "required_version inside required_providers is ignored",
Source: `terraform {
required_providers {
fake = {
required_version = "9.9.9"
}
}
}`,
OK: false,
},
{
Case: "other top-level blocks before terraform",
Source: `provider "aws" {
region = "eu-west-1"
}
resource "aws_instance" "web" {
tags = {
Name = "terraform"
}
}
terraform {
required_version = "~> 1.5"
}`,
Expected: "~> 1.5",
OK: true,
},
{
Case: "comments of all flavors",
Source: `# leading comment with terraform {
// another one with required_version = "0.0.0"
/* block comment
terraform { required_version = "0.0.1" }
*/
terraform { # trailing
/* inline */ required_version = "1.6.2" // done
}`,
Expected: "1.6.2",
OK: true,
},
{
Case: "string containing braces does not break depth tracking",
Source: `locals {
tpl = "prefix { nested "
}
terraform {
required_version = ">= 1.1"
}`,
Expected: ">= 1.1",
OK: true,
},
{
Case: "interpolation with nested string and braces",
Source: `locals {
greeting = "${var.enabled ? "y{es" : "no}"} done"
}
terraform {
required_version = "1.7.0"
}`,
Expected: "1.7.0",
OK: true,
},
{
Case: "heredoc containing a fake terraform block",
Source: `resource "local_file" "f" {
content = <<EOF
terraform {
required_version = "0.0.0"
}
EOF
}
terraform {
required_version = "1.8.1"
}`,
Expected: "1.8.1",
OK: true,
},
{
Case: "indented heredoc",
Source: `resource "local_file" "f" {
content = <<-DOC
terraform { required_version = "0.0.0" }
DOC
}
terraform {
required_version = "1.8.2"
}`,
Expected: "1.8.2",
OK: true,
},
{
Case: "no terraform block",
Source: "provider \"aws\" {\n region = \"eu-west-1\"\n}\n",
OK: false,
},
{
Case: "terraform block without required_version",
Source: "terraform {\n required_providers {\n }\n}\n",
OK: false,
},
{
Case: "empty source",
Source: "",
OK: false,
},
{
Case: "unterminated string fails closed",
Source: "terraform {\n required_version = \">= 1.0",
OK: false,
},
{
Case: "escaped quote in value",
Source: `terraform { required_version = "a\"b" }`,
Expected: `a"b`,
OK: true,
},
{
Case: "windows line endings",
Source: "terraform {\r\n required_version = \">= 1.2.3\"\r\n}\r\n",
Expected: ">= 1.2.3",
OK: true,
},
{
Case: "identifier merely containing terraform is not a block",
Source: `terraform_version = "0.0.0"
terraform {
required_version = "1.9.9"
}`,
Expected: "1.9.9",
OK: true,
},
{
Case: "second terraform block wins when first has no version",
Source: `terraform {
backend "s3" {}
}
terraform {
required_version = "1.4.4"
}`,
Expected: "1.4.4",
OK: true,
},
}
for _, tc := range cases {
version, ok := extractRequiredVersion(tc.Source)
assert.Equal(t, tc.OK, ok, tc.Case)
assert.Equal(t, tc.Expected, version, tc.Case)
}
}