@@ -6,10 +6,15 @@ package check
66import (
77 "fmt"
88 "log"
9+ "os"
910 "path"
11+ "path/filepath"
12+ "sort"
1013 "strings"
14+ "sync"
1115
1216 "github.com/fatih/color"
17+ "github.com/hashicorp/terraform-provider-azurerm/internal/tools/document-lint/md"
1318 "github.com/hashicorp/terraform-provider-azurerm/internal/tools/document-lint/model"
1419 "github.com/hashicorp/terraform-provider-azurerm/internal/tools/document-lint/schema"
1520 "github.com/hashicorp/terraform-provider-azurerm/internal/tools/document-lint/util"
@@ -112,9 +117,62 @@ func (p possibleValueDiff) Fix(line string) (result string, err error) {
112117
113118var _ Checker = (* possibleValueDiff )(nil )
114119
120+ // sortPossibleValues sorts possible values in a sensible order
121+ // For weekdays, it maintains chronological order (Monday -> Sunday)
122+ // For other values, it sorts alphabetically (case-insensitive)
123+ func sortPossibleValues (values []string ) []string {
124+ if len (values ) <= 1 {
125+ return values
126+ }
127+
128+ // Check if all values are weekdays
129+ weekdayOrder := map [string ]int {
130+ "monday" : 1 ,
131+ "tuesday" : 2 ,
132+ "wednesday" : 3 ,
133+ "thursday" : 4 ,
134+ "friday" : 5 ,
135+ "saturday" : 6 ,
136+ "sunday" : 7 ,
137+ "Monday" : 1 ,
138+ "Tuesday" : 2 ,
139+ "Wednesday" : 3 ,
140+ "Thursday" : 4 ,
141+ "Friday" : 5 ,
142+ "Saturday" : 6 ,
143+ "Sunday" : 7 ,
144+ }
145+
146+ allWeekdays := true
147+ for _ , v := range values {
148+ if _ , ok := weekdayOrder [v ]; ! ok {
149+ allWeekdays = false
150+ break
151+ }
152+ }
153+
154+ sorted := make ([]string , len (values ))
155+ copy (sorted , values )
156+
157+ if allWeekdays {
158+ sort .Slice (sorted , func (i , j int ) bool {
159+ return weekdayOrder [sorted [i ]] < weekdayOrder [sorted [j ]]
160+ })
161+ } else {
162+ sort .Slice (sorted , func (i , j int ) bool {
163+ return strings .ToLower (sorted [i ]) < strings .ToLower (sorted [j ])
164+ })
165+ }
166+
167+ return sorted
168+ }
169+
115170func patchWantEnums (want []string ) string {
116- res := make ([]string , len (want ))
117- for idx , val := range want {
171+ // Sort the values before formatting
172+ sorted := sortPossibleValues (want )
173+
174+ res := make ([]string , len (sorted ))
175+ for idx , val := range sorted {
118176 res [idx ] = "`" + val + "`"
119177 }
120178 if len (res ) == 1 {
@@ -154,12 +212,21 @@ func diffField(r *schema.Resource, mdField *model.Field, xPath []string) (res []
154212
155213 // if end property
156214 if mdField .Subs == nil {
157- want := r .PossibleValues [fullPath ]
215+ wanted := r .PossibleValues [fullPath ]
158216 docVal := mdField .PossibleValues ()
159- if missed , spare := SliceDiff (want , docVal , true ); len (missed )+ len (spare ) > 0 {
160- if ! mayExistsInDoc (mdField .Content , want ) {
217+ if missed , spare := SliceDiff (wanted , docVal , true ); len (missed )+ len (spare ) > 0 {
218+ // Check if this property has version changes before reporting error
219+ // Skip possible value diff if this property has changes in new version upgrade.
220+ // This is because it's not consistent whether possible values change should be documented or not when it's in the upgrade feature flag
221+ if hasVersionChanges (r .ResourceType , fullPath ) {
222+ log .Printf ("[SKIP] %s.%s: Skipping possible values validation due to version changes detected in upgrade guide (missed: %v, spare: %v)" ,
223+ r .ResourceType , fullPath , missed , spare )
224+ return
225+ }
226+
227+ if ! mayExistsInDoc (mdField .Content , wanted ) {
161228 base := newCheckBase (mdField .Line , fullPath , mdField )
162- res = append (res , newPossibleValueDiff (base , want , docVal , missed , spare ))
229+ res = append (res , newPossibleValueDiff (base , wanted , docVal , missed , spare ))
163230 }
164231 }
165232 return
@@ -220,3 +287,110 @@ func mayExistsInDoc(docLine string, want []string) bool {
220287 }
221288 return true
222289}
290+
291+ // hasVersionChanges checks if the field has version-related changes by parsing the upgrade guide
292+ func hasVersionChanges (resourceType , fieldPath string ) bool {
293+ upgradeGuideContent := getUpgradeGuideContent ()
294+ if upgradeGuideContent == "" {
295+ return false
296+ }
297+
298+ // Look for the resource section
299+ resourceSectionPattern := fmt .Sprintf ("### `%s`" , resourceType )
300+
301+ // Find the resource section
302+ lines := strings .Split (upgradeGuideContent , "\n " )
303+ resourceSectionStart := - 1
304+
305+ for i , line := range lines {
306+ if strings .Contains (line , resourceSectionPattern ) {
307+ resourceSectionStart = i
308+ break
309+ }
310+ }
311+
312+ // Resource not found in upgrade guide
313+ if resourceSectionStart == - 1 {
314+ return false
315+ }
316+
317+ // Find the end of this resource section (next resource or major section)
318+ resourceSectionEnd := len (lines )
319+ for i := resourceSectionStart + 1 ; i < len (lines ); i ++ {
320+ line := lines [i ]
321+ if strings .HasPrefix (line , "### `azurerm_" ) || strings .HasPrefix (line , "## " ) {
322+ resourceSectionEnd = i
323+ break
324+ }
325+ }
326+
327+ // Get the content of this resource section
328+ resourceSection := strings .Join (lines [resourceSectionStart :resourceSectionEnd ], "\n " )
329+
330+ // Check if the property is mentioned in this resource section
331+ propertyPatterns := []string {
332+ fmt .Sprintf ("`%s`" , fieldPath ), // `property_name`
333+ fmt .Sprintf (" %s " , fieldPath ), // property_name with spaces
334+ fmt .Sprintf (".%s " , fieldPath ), // .property_name
335+ fmt .Sprintf ("`%s." , fieldPath ), // `property_name.
336+ }
337+
338+ // For nested properties like "site_config.remote_debugging_version", also check the last part
339+ if strings .Contains (fieldPath , "." ) {
340+ parts := strings .Split (fieldPath , "." )
341+ lastPart := parts [len (parts )- 1 ]
342+ propertyPatterns = append (propertyPatterns ,
343+ fmt .Sprintf ("`%s`" , lastPart ), // `last_part`
344+ fmt .Sprintf (" %s " , lastPart ), // last_part with spaces
345+ )
346+ }
347+
348+ // Check if any pattern is found in the resource section
349+ for _ , pattern := range propertyPatterns {
350+ if strings .Contains (resourceSection , pattern ) {
351+ return true
352+ }
353+ }
354+
355+ return false
356+ }
357+
358+ var (
359+ upgradeGuideContent string
360+ loadUpgradeGuideOnce sync.Once
361+ )
362+
363+ // getUpgradeGuideContent loads and caches the upgrade guide content
364+ func getUpgradeGuideContent () string {
365+ loadUpgradeGuideOnce .Do (func () {
366+ docsDir := md .DocDir ()
367+
368+ if upgradeFile := findUpgradeGuide (docsDir ); upgradeFile != "" {
369+ if content , err := os .ReadFile (upgradeFile ); err == nil {
370+ upgradeGuideContent = string (content )
371+ log .Printf ("Loaded upgrade guide from: %s" , upgradeFile )
372+ return
373+ }
374+ }
375+
376+ log .Printf ("Warning: Could not find any *-upgrade-guide.html.markdown file in docs directory: %s" , docsDir )
377+ upgradeGuideContent = ""
378+ })
379+
380+ return upgradeGuideContent
381+ }
382+
383+ // findUpgradeGuide searches for upgrade guide files in the given directory
384+ func findUpgradeGuide (dir string ) string {
385+ if _ , err := os .Stat (dir ); os .IsNotExist (err ) {
386+ return ""
387+ }
388+
389+ pattern := filepath .Join (dir , "*-upgrade-guide.html.markdown" )
390+ matches , err := filepath .Glob (pattern )
391+ if err != nil || len (matches ) == 0 {
392+ return ""
393+ }
394+
395+ return matches [0 ]
396+ }
0 commit comments