mirror of
https://github.com/JanDeDobbeleer/oh-my-posh.git
synced 2026-08-24 10:14:12 -05:00
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:
@@ -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
@@ -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=
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user