github.com/terraform-linters/tflint-plugin-sdk@v0.22.0/internal/fixer_test.go (about)

     1  package internal
     2  
     3  import (
     4  	"math/big"
     5  	"testing"
     6  
     7  	"github.com/google/go-cmp/cmp"
     8  	"github.com/hashicorp/hcl/v2"
     9  	"github.com/hashicorp/hcl/v2/hclsyntax"
    10  	"github.com/terraform-linters/tflint-plugin-sdk/hclext"
    11  	"github.com/terraform-linters/tflint-plugin-sdk/tflint"
    12  	"github.com/zclconf/go-cty/cty"
    13  )
    14  
    15  func TestReplaceText(t *testing.T) {
    16  	// default error check helper
    17  	neverHappend := func(err error) bool { return err != nil }
    18  
    19  	tests := []struct {
    20  		name     string
    21  		sources  map[string]string
    22  		fix      func(*Fixer) error
    23  		want     map[string]string
    24  		errCheck func(error) bool
    25  	}{
    26  		{
    27  			name: "no change",
    28  			sources: map[string]string{
    29  				"main.tf": "// comment",
    30  			},
    31  			fix: func(fixer *Fixer) error {
    32  				return nil
    33  			},
    34  			want:     map[string]string{},
    35  			errCheck: neverHappend,
    36  		},
    37  		{
    38  			name: "no shift",
    39  			sources: map[string]string{
    40  				"main.tf": "// comment",
    41  			},
    42  			fix: func(fixer *Fixer) error {
    43  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 0}, End: hcl.Pos{Byte: 2}}, "##")
    44  			},
    45  			want: map[string]string{
    46  				"main.tf": "## comment",
    47  			},
    48  			errCheck: neverHappend,
    49  		},
    50  		{
    51  			name: "shift left",
    52  			sources: map[string]string{
    53  				"main.tf": "// comment",
    54  			},
    55  			fix: func(fixer *Fixer) error {
    56  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 0}, End: hcl.Pos{Byte: 2}}, "#")
    57  			},
    58  			want: map[string]string{
    59  				"main.tf": "# comment",
    60  			},
    61  			errCheck: neverHappend,
    62  		},
    63  		{
    64  			name: "shift right",
    65  			sources: map[string]string{
    66  				"main.tf": "# comment",
    67  			},
    68  			fix: func(fixer *Fixer) error {
    69  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 0}, End: hcl.Pos{Byte: 1}}, "//")
    70  			},
    71  			want: map[string]string{
    72  				"main.tf": "// comment",
    73  			},
    74  			errCheck: neverHappend,
    75  		},
    76  		{
    77  			name: "no shift + shift left",
    78  			sources: map[string]string{
    79  				"main.tf": `
    80  // comment
    81  // comment2`,
    82  			},
    83  			fix: func(fixer *Fixer) error {
    84  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 3}}, "##")
    85  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 12}, End: hcl.Pos{Byte: 14}}, "#")
    86  			},
    87  			want: map[string]string{
    88  				"main.tf": `
    89  ## comment
    90  # comment2`,
    91  			},
    92  			errCheck: neverHappend,
    93  		},
    94  		{
    95  			name: "no shift + shift right",
    96  			sources: map[string]string{
    97  				"main.tf": `
    98  ## comment
    99  # comment2`,
   100  			},
   101  			fix: func(fixer *Fixer) error {
   102  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 3}}, "//")
   103  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 12}, End: hcl.Pos{Byte: 13}}, "//")
   104  			},
   105  			want: map[string]string{
   106  				"main.tf": `
   107  // comment
   108  // comment2`,
   109  			},
   110  			errCheck: neverHappend,
   111  		},
   112  		{
   113  			name: "shift left + shift left",
   114  			sources: map[string]string{
   115  				"main.tf": `
   116  // comment
   117  // comment2`,
   118  			},
   119  			fix: func(fixer *Fixer) error {
   120  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 3}}, "#")
   121  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 12}, End: hcl.Pos{Byte: 14}}, "#")
   122  			},
   123  			want: map[string]string{
   124  				"main.tf": `
   125  # comment
   126  # comment2`,
   127  			},
   128  			errCheck: neverHappend,
   129  		},
   130  		{
   131  			name: "shift left + shift right",
   132  			sources: map[string]string{
   133  				"main.tf": `
   134  // comment
   135  # comment2`,
   136  			},
   137  			fix: func(fixer *Fixer) error {
   138  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 3}}, "#")
   139  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 12}, End: hcl.Pos{Byte: 13}}, "//")
   140  			},
   141  			want: map[string]string{
   142  				"main.tf": `
   143  # comment
   144  // comment2`,
   145  			},
   146  			errCheck: neverHappend,
   147  		},
   148  		{
   149  			name: "shift right + shift left",
   150  			sources: map[string]string{
   151  				"main.tf": `
   152  # comment
   153  // comment2`,
   154  			},
   155  			fix: func(fixer *Fixer) error {
   156  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 2}}, "//")
   157  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 11}, End: hcl.Pos{Byte: 13}}, "#")
   158  			},
   159  			want: map[string]string{
   160  				"main.tf": `
   161  // comment
   162  # comment2`,
   163  			},
   164  			errCheck: neverHappend,
   165  		},
   166  		{
   167  			name: "shift right + shift right",
   168  			sources: map[string]string{
   169  				"main.tf": `
   170  # comment
   171  # comment2`,
   172  			},
   173  			fix: func(fixer *Fixer) error {
   174  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 2}}, "//")
   175  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 11}, End: hcl.Pos{Byte: 12}}, "//")
   176  			},
   177  			want: map[string]string{
   178  				"main.tf": `
   179  // comment
   180  // comment2`,
   181  			},
   182  			errCheck: neverHappend,
   183  		},
   184  		{
   185  			name: "shift left + shift left + shift left",
   186  			sources: map[string]string{
   187  				"main.tf": `
   188  // comment
   189  // comment2
   190  // comment3`,
   191  			},
   192  			fix: func(fixer *Fixer) error {
   193  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 3}}, "#")
   194  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 12}, End: hcl.Pos{Byte: 14}}, "#")
   195  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 24}, End: hcl.Pos{Byte: 26}}, "#")
   196  			},
   197  			want: map[string]string{
   198  				"main.tf": `
   199  # comment
   200  # comment2
   201  # comment3`,
   202  			},
   203  			errCheck: neverHappend,
   204  		},
   205  		{
   206  			name: "shift left + shift left + shift right",
   207  			sources: map[string]string{
   208  				"main.tf": `
   209  // comment
   210  // comment2
   211  # comment3`,
   212  			},
   213  			fix: func(fixer *Fixer) error {
   214  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 3}}, "#")
   215  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 12}, End: hcl.Pos{Byte: 14}}, "#")
   216  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 24}, End: hcl.Pos{Byte: 25}}, "//")
   217  			},
   218  			want: map[string]string{
   219  				"main.tf": `
   220  # comment
   221  # comment2
   222  // comment3`,
   223  			},
   224  			errCheck: neverHappend,
   225  		},
   226  		{
   227  			name: "shift left + shift right + shift left",
   228  			sources: map[string]string{
   229  				"main.tf": `
   230  // comment
   231  # comment2
   232  // comment3`,
   233  			},
   234  			fix: func(fixer *Fixer) error {
   235  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 3}}, "#")
   236  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 12}, End: hcl.Pos{Byte: 13}}, "//")
   237  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 23}, End: hcl.Pos{Byte: 25}}, "#")
   238  			},
   239  			want: map[string]string{
   240  				"main.tf": `
   241  # comment
   242  // comment2
   243  # comment3`,
   244  			},
   245  			errCheck: neverHappend,
   246  		},
   247  		{
   248  			name: "shift left + shift right + shift right",
   249  			sources: map[string]string{
   250  				"main.tf": `
   251  // comment
   252  # comment2
   253  # comment3`,
   254  			},
   255  			fix: func(fixer *Fixer) error {
   256  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 3}}, "#")
   257  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 12}, End: hcl.Pos{Byte: 13}}, "//")
   258  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 23}, End: hcl.Pos{Byte: 24}}, "//")
   259  			},
   260  			want: map[string]string{
   261  				"main.tf": `
   262  # comment
   263  // comment2
   264  // comment3`,
   265  			},
   266  			errCheck: neverHappend,
   267  		},
   268  		{
   269  			name: "shift right + shift left + shift left",
   270  			sources: map[string]string{
   271  				"main.tf": `
   272  # comment
   273  // comment2
   274  // comment3`,
   275  			},
   276  			fix: func(fixer *Fixer) error {
   277  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 2}}, "//")
   278  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 11}, End: hcl.Pos{Byte: 13}}, "#")
   279  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 23}, End: hcl.Pos{Byte: 25}}, "#")
   280  			},
   281  			want: map[string]string{
   282  				"main.tf": `
   283  // comment
   284  # comment2
   285  # comment3`,
   286  			},
   287  			errCheck: neverHappend,
   288  		},
   289  		{
   290  			name: "shift right + shift left + shift right",
   291  			sources: map[string]string{
   292  				"main.tf": `
   293  # comment
   294  // comment2
   295  # comment3`,
   296  			},
   297  			fix: func(fixer *Fixer) error {
   298  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 2}}, "//")
   299  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 11}, End: hcl.Pos{Byte: 13}}, "#")
   300  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 23}, End: hcl.Pos{Byte: 24}}, "//")
   301  			},
   302  			want: map[string]string{
   303  				"main.tf": `
   304  // comment
   305  # comment2
   306  // comment3`,
   307  			},
   308  			errCheck: neverHappend,
   309  		},
   310  		{
   311  			name: "shift right + shift right + shift left",
   312  			sources: map[string]string{
   313  				"main.tf": `
   314  # comment
   315  # comment2
   316  // comment3`,
   317  			},
   318  			fix: func(fixer *Fixer) error {
   319  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 2}}, "//")
   320  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 11}, End: hcl.Pos{Byte: 12}}, "//")
   321  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 22}, End: hcl.Pos{Byte: 24}}, "#")
   322  			},
   323  			want: map[string]string{
   324  				"main.tf": `
   325  // comment
   326  // comment2
   327  # comment3`,
   328  			},
   329  			errCheck: neverHappend,
   330  		},
   331  		{
   332  			name: "shift right + shift right + shift right",
   333  			sources: map[string]string{
   334  				"main.tf": `
   335  # comment
   336  # comment2
   337  # comment3`,
   338  			},
   339  			fix: func(fixer *Fixer) error {
   340  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 2}}, "//")
   341  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 11}, End: hcl.Pos{Byte: 12}}, "//")
   342  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 22}, End: hcl.Pos{Byte: 23}}, "//")
   343  			},
   344  			want: map[string]string{
   345  				"main.tf": `
   346  // comment
   347  // comment2
   348  // comment3`,
   349  			},
   350  			errCheck: neverHappend,
   351  		},
   352  		{
   353  			name: "change order",
   354  			sources: map[string]string{
   355  				"main.tf": `
   356  # comment
   357  # comment2
   358  # comment3`,
   359  			},
   360  			fix: func(fixer *Fixer) error {
   361  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 11}, End: hcl.Pos{Byte: 12}}, "//")
   362  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 2}}, "//")
   363  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 22}, End: hcl.Pos{Byte: 23}}, "//")
   364  			},
   365  			want: map[string]string{
   366  				"main.tf": `
   367  // comment
   368  // comment2
   369  // comment3`,
   370  			},
   371  			errCheck: neverHappend,
   372  		},
   373  		{
   374  			name: "shift left (boundary)",
   375  			sources: map[string]string{
   376  				"main.tf": `"Hellooo, world!"`,
   377  			},
   378  			fix: func(fixer *Fixer) error {
   379  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 8}}, "Hello")
   380  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 8}, End: hcl.Pos{Byte: 16}}, ", you and world!")
   381  			},
   382  			want: map[string]string{
   383  				"main.tf": `"Hello, you and world!"`,
   384  			},
   385  			errCheck: neverHappend,
   386  		},
   387  		{
   388  			name: "shift right (boundary)",
   389  			sources: map[string]string{
   390  				"main.tf": `"Hello, world!"`,
   391  			},
   392  			fix: func(fixer *Fixer) error {
   393  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 6}}, "Hellooo")
   394  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 6}, End: hcl.Pos{Byte: 14}}, ", you and world!")
   395  			},
   396  			want: map[string]string{
   397  				"main.tf": `"Hellooo, you and world!"`,
   398  			},
   399  			errCheck: neverHappend,
   400  		},
   401  		{
   402  			name: "overlapping",
   403  			sources: map[string]string{
   404  				"main.tf": `"Hello, world!"`,
   405  			},
   406  			fix: func(fixer *Fixer) error {
   407  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 8}}, "Hellooo, ")
   408  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 6}, End: hcl.Pos{Byte: 14}}, ", you and world!")
   409  			},
   410  			want: map[string]string{
   411  				"main.tf": `"Hellooo, world!"`,
   412  			},
   413  			errCheck: func(err error) bool {
   414  				return err == nil || err.Error() != "range overlaps with a previous rewrite range: main.tf:0,0-0"
   415  			},
   416  		},
   417  		{
   418  			name: "same range",
   419  			sources: map[string]string{
   420  				"main.tf": `"Hello, world!"`,
   421  			},
   422  			fix: func(fixer *Fixer) error {
   423  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 6}}, "hello")
   424  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 6}}, "HELLO")
   425  			},
   426  			want: map[string]string{
   427  				"main.tf": `"HELLO, world!"`,
   428  			},
   429  			errCheck: neverHappend,
   430  		},
   431  		{
   432  			name: "same range (shift left)",
   433  			sources: map[string]string{
   434  				"main.tf": `"Hellooo, world!"`,
   435  			},
   436  			fix: func(fixer *Fixer) error {
   437  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 8}}, "hello")
   438  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 8}}, "HELLO")
   439  			},
   440  			want: map[string]string{
   441  				"main.tf": `"HELLO, world!"`,
   442  			},
   443  			errCheck: neverHappend,
   444  		},
   445  		{
   446  			name: "same range (shift right)",
   447  			sources: map[string]string{
   448  				"main.tf": `"Hello, world!"`,
   449  			},
   450  			fix: func(fixer *Fixer) error {
   451  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 6}}, "hellooo")
   452  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 6}}, "HELLOOO")
   453  			},
   454  			want: map[string]string{
   455  				"main.tf": `"HELLOOO, world!"`,
   456  			},
   457  			errCheck: neverHappend,
   458  		},
   459  		{
   460  			name: "shift after same range",
   461  			sources: map[string]string{
   462  				"main.tf": `"Hello, world!"`,
   463  			},
   464  			fix: func(fixer *Fixer) error {
   465  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 6}}, "hellooo")
   466  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 6}}, "HELLOOO")
   467  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 6}, End: hcl.Pos{Byte: 14}}, ", you and world!")
   468  			},
   469  			want: map[string]string{
   470  				"main.tf": `"HELLOOO, you and world!"`,
   471  			},
   472  			errCheck: neverHappend,
   473  		},
   474  		{
   475  			name: "same range after shift",
   476  			sources: map[string]string{
   477  				"main.tf": `"Hello, world!"`,
   478  			},
   479  			fix: func(fixer *Fixer) error {
   480  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 6}}, "hellooo")
   481  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 8}, End: hcl.Pos{Byte: 13}}, "wooorld")
   482  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 6}}, "Hellooo")
   483  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 8}, End: hcl.Pos{Byte: 13}}, "Wooorld")
   484  			},
   485  			want: map[string]string{
   486  				"main.tf": `"Hellooo, Wooorld!"`,
   487  			},
   488  			errCheck: neverHappend,
   489  		},
   490  		{
   491  			name: "multibyte",
   492  			sources: map[string]string{
   493  				"main.tf": `"Hello, world!"`,
   494  			},
   495  			fix: func(fixer *Fixer) error {
   496  				fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 6}}, "こんにちは")
   497  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 8}, End: hcl.Pos{Byte: 13}}, "世界")
   498  			},
   499  			want: map[string]string{
   500  				"main.tf": `"こんにちは, 世界!"`,
   501  			},
   502  			errCheck: neverHappend,
   503  		},
   504  		{
   505  			name: "file not found",
   506  			sources: map[string]string{
   507  				"main.tf": `"Hello, world!"`,
   508  			},
   509  			fix: func(fixer *Fixer) error {
   510  				return fixer.ReplaceText(hcl.Range{Filename: "template.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 6}}, "hello")
   511  			},
   512  			want: map[string]string{},
   513  			errCheck: func(err error) bool {
   514  				return err == nil || err.Error() != "file not found: template.tf"
   515  			},
   516  		},
   517  		{
   518  			name: "multiple string literals",
   519  			sources: map[string]string{
   520  				"main.tf": `(foo)(bar)`,
   521  			},
   522  			fix: func(fixer *Fixer) error {
   523  				return fixer.ReplaceText(
   524  					hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 0}, End: hcl.Pos{Byte: 10}},
   525  					"[",
   526  					"foo",
   527  					"]",
   528  					"[",
   529  					"bar",
   530  					"]",
   531  				)
   532  			},
   533  			want: map[string]string{
   534  				"main.tf": `[foo][bar]`,
   535  			},
   536  			errCheck: neverHappend,
   537  		},
   538  		{
   539  			name: "literals with text nodes",
   540  			sources: map[string]string{
   541  				"main.tf": `(foo)(bar)`,
   542  			},
   543  			fix: func(fixer *Fixer) error {
   544  				if err := fixer.ReplaceText(
   545  					hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 0}, End: hcl.Pos{Byte: 10}},
   546  					"[",
   547  					tflint.TextNode{Bytes: []byte("foo"), Range: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 4}}},
   548  					"]",
   549  					"[",
   550  					tflint.TextNode{Bytes: []byte("bar"), Range: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 6}, End: hcl.Pos{Byte: 9}}},
   551  					"]",
   552  				); err != nil {
   553  					return err
   554  				}
   555  				// The replacement is not overlapped because the "foo" is not replaced in the previous replacement.
   556  				if err := fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 4}}, "bar"); err != nil {
   557  					return err
   558  				}
   559  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 6}, End: hcl.Pos{Byte: 9}}, "baz")
   560  			},
   561  			want: map[string]string{
   562  				"main.tf": `[bar][baz]`,
   563  			},
   564  			errCheck: neverHappend,
   565  		},
   566  		{
   567  			name: "only text nodes",
   568  			sources: map[string]string{
   569  				"main.tf": `(foo)(bar)`,
   570  			},
   571  			fix: func(fixer *Fixer) error {
   572  				if err := fixer.ReplaceText(
   573  					hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 0}, End: hcl.Pos{Byte: 10}},
   574  					tflint.TextNode{Bytes: []byte("foo"), Range: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 4}}},
   575  					tflint.TextNode{Bytes: []byte("bar"), Range: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 6}, End: hcl.Pos{Byte: 9}}},
   576  				); err != nil {
   577  					return err
   578  				}
   579  				// The replacement is not overlapped because the "foo" is not replaced in the previous replacement.
   580  				if err := fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 4}}, "bar"); err != nil {
   581  					return err
   582  				}
   583  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 6}, End: hcl.Pos{Byte: 9}}, "baz")
   584  			},
   585  			want: map[string]string{
   586  				"main.tf": `barbaz`,
   587  			},
   588  			errCheck: neverHappend,
   589  		},
   590  		{
   591  			name: "unordered text nodes",
   592  			sources: map[string]string{
   593  				"main.tf": `(foo)(bar)`,
   594  			},
   595  			fix: func(fixer *Fixer) error {
   596  				if err := fixer.ReplaceText(
   597  					hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 0}, End: hcl.Pos{Byte: 10}},
   598  					"[",
   599  					tflint.TextNode{Bytes: []byte("bar"), Range: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 6}, End: hcl.Pos{Byte: 9}}},
   600  					"]",
   601  					"[",
   602  					tflint.TextNode{Bytes: []byte("foo"), Range: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 4}}},
   603  					"]",
   604  				); err != nil {
   605  					return err
   606  				}
   607  				// The replacement is not overlapped because the "bar" is not replaced in the previous replacement.
   608  				if err := fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 6}, End: hcl.Pos{Byte: 9}}, "baz"); err != nil {
   609  					return err
   610  				}
   611  				// The replacement is overlapped because the "foo" is replaced in the previous replacement.
   612  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 4}}, "bar")
   613  			},
   614  			want: map[string]string{
   615  				"main.tf": `[baz][foo]`,
   616  			},
   617  			errCheck: func(err error) bool {
   618  				return err == nil || err.Error() != "range overlaps with a previous rewrite range: main.tf:0,0-0"
   619  			},
   620  		},
   621  		{
   622  			name: "out of range text node",
   623  			sources: map[string]string{
   624  				"main.tf": `foo`,
   625  			},
   626  			fix: func(fixer *Fixer) error {
   627  				return fixer.ReplaceText(
   628  					hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 0}, End: hcl.Pos{Byte: 3}},
   629  					tflint.TextNode{Bytes: []byte("baz"), Range: hcl.Range{Filename: "template.tf", Start: hcl.Pos{Byte: 0}, End: hcl.Pos{Byte: 3}}},
   630  				)
   631  			},
   632  			want: map[string]string{
   633  				"main.tf": `baz`,
   634  			},
   635  			errCheck: neverHappend,
   636  		},
   637  		{
   638  			name: "text node with the same range",
   639  			sources: map[string]string{
   640  				"main.tf": `foo`,
   641  			},
   642  			fix: func(fixer *Fixer) error {
   643  				return fixer.ReplaceText(
   644  					hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 0}, End: hcl.Pos{Byte: 3}},
   645  					tflint.TextNode{Bytes: []byte("foo"), Range: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 0}, End: hcl.Pos{Byte: 3}}},
   646  				)
   647  			},
   648  			want:     map[string]string{},
   649  			errCheck: neverHappend,
   650  		},
   651  	}
   652  
   653  	for _, test := range tests {
   654  		t.Run(test.name, func(t *testing.T) {
   655  			input := map[string][]byte{}
   656  			for filename, source := range test.sources {
   657  				input[filename] = []byte(source)
   658  			}
   659  			fixer := NewFixer(input)
   660  
   661  			err := test.fix(fixer)
   662  			if test.errCheck(err) {
   663  				t.Fatalf("failed to check error: %s", err)
   664  			}
   665  
   666  			changes := map[string]string{}
   667  			for filename, source := range fixer.changes {
   668  				changes[filename] = string(source)
   669  			}
   670  			if diff := cmp.Diff(test.want, changes); diff != "" {
   671  				t.Errorf(diff)
   672  			}
   673  		})
   674  	}
   675  }
   676  
   677  func TestInsertText(t *testing.T) {
   678  	// default error check helper
   679  	neverHappend := func(err error) bool { return err != nil }
   680  
   681  	tests := []struct {
   682  		name     string
   683  		sources  map[string]string
   684  		fix      func(*Fixer) error
   685  		want     map[string]string
   686  		errCheck func(error) bool
   687  	}{
   688  		{
   689  			name: "insert before by InsertTextBefore",
   690  			sources: map[string]string{
   691  				"main.tf": `"world!"`,
   692  			},
   693  			fix: func(fixer *Fixer) error {
   694  				return fixer.InsertTextBefore(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 6}}, "Hello, ")
   695  			},
   696  			want: map[string]string{
   697  				"main.tf": `"Hello, world!"`,
   698  			},
   699  			errCheck: neverHappend,
   700  		},
   701  		{
   702  			name: "insert after by InsertTextBefore",
   703  			sources: map[string]string{
   704  				"main.tf": `"Hello"`,
   705  			},
   706  			fix: func(fixer *Fixer) error {
   707  				return fixer.InsertTextBefore(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 6}, End: hcl.Pos{Byte: 7}}, ", world!")
   708  			},
   709  			want: map[string]string{
   710  				"main.tf": `"Hello, world!"`,
   711  			},
   712  			errCheck: neverHappend,
   713  		},
   714  		{
   715  			name: "insert before by InsertTextAfter",
   716  			sources: map[string]string{
   717  				"main.tf": `"world!"`,
   718  			},
   719  			fix: func(fixer *Fixer) error {
   720  				return fixer.InsertTextAfter(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 0}, End: hcl.Pos{Byte: 1}}, "Hello, ")
   721  			},
   722  			want: map[string]string{
   723  				"main.tf": `"Hello, world!"`,
   724  			},
   725  			errCheck: neverHappend,
   726  		},
   727  		{
   728  			name: "insert after by InsertTextAfter",
   729  			sources: map[string]string{
   730  				"main.tf": `"Hello"`,
   731  			},
   732  			fix: func(fixer *Fixer) error {
   733  				return fixer.InsertTextAfter(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 6}}, ", world!")
   734  			},
   735  			want: map[string]string{
   736  				"main.tf": `"Hello, world!"`,
   737  			},
   738  			errCheck: neverHappend,
   739  		},
   740  	}
   741  
   742  	for _, test := range tests {
   743  		t.Run(test.name, func(t *testing.T) {
   744  			input := map[string][]byte{}
   745  			for filename, source := range test.sources {
   746  				input[filename] = []byte(source)
   747  			}
   748  			fixer := NewFixer(input)
   749  
   750  			err := test.fix(fixer)
   751  			if test.errCheck(err) {
   752  				t.Fatalf("failed to check error: %s", err)
   753  			}
   754  
   755  			changes := map[string]string{}
   756  			for filename, source := range fixer.changes {
   757  				changes[filename] = string(source)
   758  			}
   759  			if diff := cmp.Diff(test.want, changes); diff != "" {
   760  				t.Errorf(diff)
   761  			}
   762  		})
   763  	}
   764  }
   765  
   766  func TestRemove(t *testing.T) {
   767  	// default error check helper
   768  	neverHappend := func(err error) bool { return err != nil }
   769  
   770  	tests := []struct {
   771  		name     string
   772  		sources  map[string]string
   773  		fix      func(*Fixer) error
   774  		want     map[string]string
   775  		errCheck func(error) bool
   776  	}{
   777  		{
   778  			name: "remove",
   779  			sources: map[string]string{
   780  				"main.tf": `"Hello, world!"`,
   781  			},
   782  			fix: func(fixer *Fixer) error {
   783  				return fixer.Remove(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 6}, End: hcl.Pos{Byte: 14}})
   784  			},
   785  			want: map[string]string{
   786  				"main.tf": `"Hello"`,
   787  			},
   788  			errCheck: neverHappend,
   789  		},
   790  		{
   791  			name: "remove and shift",
   792  			sources: map[string]string{
   793  				"main.tf": `"Hello, world!"`,
   794  			},
   795  			fix: func(fixer *Fixer) error {
   796  				fixer.Remove(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 1}, End: hcl.Pos{Byte: 8}})
   797  				return fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 8}, End: hcl.Pos{Byte: 14}}, "WORLD!!")
   798  			},
   799  			want: map[string]string{
   800  				"main.tf": `"WORLD!!"`,
   801  			},
   802  			errCheck: neverHappend,
   803  		},
   804  	}
   805  
   806  	for _, test := range tests {
   807  		t.Run(test.name, func(t *testing.T) {
   808  			input := map[string][]byte{}
   809  			for filename, source := range test.sources {
   810  				input[filename] = []byte(source)
   811  			}
   812  			fixer := NewFixer(input)
   813  
   814  			err := test.fix(fixer)
   815  			if test.errCheck(err) {
   816  				t.Fatalf("failed to check error: %s", err)
   817  			}
   818  
   819  			changes := map[string]string{}
   820  			for filename, source := range fixer.changes {
   821  				changes[filename] = string(source)
   822  			}
   823  			if diff := cmp.Diff(test.want, changes); diff != "" {
   824  				t.Errorf(diff)
   825  			}
   826  		})
   827  	}
   828  }
   829  
   830  func TestRemoveAttribute(t *testing.T) {
   831  	// default error check helper
   832  	neverHappend := func(err error) bool { return err != nil }
   833  	// helper to get "foo" attribute in locals
   834  	getFooAttributeInLocals := func(body hcl.Body) (*hcl.Attribute, hcl.Diagnostics) {
   835  		content, _, diags := body.PartialContent(&hcl.BodySchema{
   836  			Blocks: []hcl.BlockHeaderSchema{{Type: "locals"}},
   837  		})
   838  		if diags.HasErrors() {
   839  			return nil, diags
   840  		}
   841  		attributes, diags := content.Blocks[0].Body.JustAttributes()
   842  		if diags.HasErrors() {
   843  			return nil, diags
   844  		}
   845  		return attributes["foo"], nil
   846  	}
   847  
   848  	tests := []struct {
   849  		name     string
   850  		source   string
   851  		getAttr  func(hcl.Body) (*hcl.Attribute, hcl.Diagnostics)
   852  		want     string
   853  		errCheck func(error) bool
   854  	}{
   855  		{
   856  			name:   "remove attribute",
   857  			source: `foo = 1`,
   858  			getAttr: func(body hcl.Body) (*hcl.Attribute, hcl.Diagnostics) {
   859  				attributes, diags := body.JustAttributes()
   860  				if diags.HasErrors() {
   861  					return nil, diags
   862  				}
   863  				return attributes["foo"], nil
   864  			},
   865  			want:     ``,
   866  			errCheck: neverHappend,
   867  		},
   868  		{
   869  			name: "remove attribute within block",
   870  			source: `
   871  locals {
   872    foo = 1
   873  }`,
   874  			getAttr: getFooAttributeInLocals,
   875  			want: `
   876  locals {
   877  }`,
   878  			errCheck: neverHappend,
   879  		},
   880  		{
   881  			name: "remove attribute with trailing comment",
   882  			source: `
   883  locals {
   884    foo = 1 # comment
   885  }`,
   886  			getAttr: getFooAttributeInLocals,
   887  			want: `
   888  locals {
   889  }`,
   890  			errCheck: neverHappend,
   891  		},
   892  		{
   893  			name: "remove attribute with trailing legacy comment",
   894  			source: `
   895  locals {
   896    foo = 1 // comment
   897  }`,
   898  			getAttr: getFooAttributeInLocals,
   899  			want: `
   900  locals {
   901  }`,
   902  			errCheck: neverHappend,
   903  		},
   904  		{
   905  			name: "remove attribute with trailing multiline comment",
   906  			source: `
   907  locals {
   908    foo = 1 /* comment */
   909  }`,
   910  			getAttr: getFooAttributeInLocals,
   911  			want: `
   912  locals {
   913  }`,
   914  			errCheck: neverHappend,
   915  		},
   916  		{
   917  			name: "remove attribute with next line comment",
   918  			source: `
   919  locals {
   920    foo = 1
   921    # comment
   922  }`,
   923  			getAttr: getFooAttributeInLocals,
   924  			want: `
   925  locals {
   926    # comment
   927  }`,
   928  			errCheck: neverHappend,
   929  		},
   930  		{
   931  			name: "remove attribute with prefix comment",
   932  			source: `
   933  locals {
   934  /* comment */ foo = 1
   935  }`,
   936  			getAttr: getFooAttributeInLocals,
   937  			want: `
   938  locals {
   939  }`,
   940  			errCheck: neverHappend,
   941  		},
   942  		{
   943  			name: "remove attribute with previous line comment",
   944  			source: `
   945  locals {
   946    # comment
   947    foo = 1
   948  }`,
   949  			getAttr: getFooAttributeInLocals,
   950  			want: `
   951  locals {
   952  }`,
   953  			errCheck: neverHappend,
   954  		},
   955  		{
   956  			name: "remove attribute with previous multiple line comments",
   957  			source: `
   958  locals {
   959    # comment
   960    # comment
   961    foo = 1
   962  }`,
   963  			getAttr: getFooAttributeInLocals,
   964  			want: `
   965  locals {
   966  }`,
   967  			errCheck: neverHappend,
   968  		},
   969  		{
   970  			name: "remove attribute with previous multiline comment",
   971  			source: `
   972  locals {
   973    /* comment */
   974    foo = 1
   975  }`,
   976  			getAttr: getFooAttributeInLocals,
   977  			// This is the same behavior as hclwrite.RemoveAttribute.
   978  			want: `
   979  locals {
   980    /* comment */
   981  }`,
   982  			errCheck: neverHappend,
   983  		},
   984  		{
   985  			name: "remove attribute after attribute with trailing comment",
   986  			source: `
   987  locals {
   988    bar = 1 # comment
   989    foo = 1
   990  }`,
   991  			getAttr: getFooAttributeInLocals,
   992  			want: `
   993  locals {
   994    bar = 1 # comment
   995  }`,
   996  			errCheck: neverHappend,
   997  		},
   998  		{
   999  			name:     "remove attribute within inline block",
  1000  			source:   `locals { foo = 1 }`,
  1001  			getAttr:  getFooAttributeInLocals,
  1002  			want:     `locals {}`,
  1003  			errCheck: neverHappend,
  1004  		},
  1005  		{
  1006  			name: "remove attribute in the middle of attributes",
  1007  			source: `
  1008  locals {
  1009    bar = 1
  1010    foo = 1
  1011    baz = 1
  1012  }`,
  1013  			getAttr: getFooAttributeInLocals,
  1014  			want: `
  1015  locals {
  1016    bar = 1
  1017    baz = 1
  1018  }`,
  1019  			errCheck: neverHappend,
  1020  		},
  1021  	}
  1022  
  1023  	for _, test := range tests {
  1024  		t.Run(test.name, func(t *testing.T) {
  1025  			file, diags := hclsyntax.ParseConfig([]byte(test.source), "main.tf", hcl.InitialPos)
  1026  			if diags.HasErrors() {
  1027  				t.Fatalf("failed to parse HCL: %s", diags)
  1028  			}
  1029  			attr, diags := test.getAttr(file.Body)
  1030  			if diags.HasErrors() {
  1031  				t.Fatalf("failed to get attribute: %s", diags)
  1032  			}
  1033  
  1034  			fixer := NewFixer(map[string][]byte{"main.tf": []byte(test.source)})
  1035  
  1036  			err := fixer.RemoveAttribute(attr)
  1037  			if test.errCheck(err) {
  1038  				t.Fatalf("failed to check error: %s", err)
  1039  			}
  1040  
  1041  			if diff := cmp.Diff(test.want, string(fixer.changes["main.tf"])); diff != "" {
  1042  				t.Errorf(diff)
  1043  			}
  1044  		})
  1045  	}
  1046  }
  1047  
  1048  func TestRemoveBlock(t *testing.T) {
  1049  	// default error check helper
  1050  	neverHappend := func(err error) bool { return err != nil }
  1051  	// getFirstBlock returns the first block in the given body.
  1052  	getFirstBlock := func(body hcl.Body) (*hcl.Block, hcl.Diagnostics) {
  1053  		content, _, diags := body.PartialContent(&hcl.BodySchema{
  1054  			Blocks: []hcl.BlockHeaderSchema{{Type: "block"}},
  1055  		})
  1056  		if diags.HasErrors() {
  1057  			return nil, diags
  1058  		}
  1059  		return content.Blocks[0], nil
  1060  	}
  1061  	// getNestedBlock returns the nested block in the given body.
  1062  	getNestedBlock := func(body hcl.Body) (*hcl.Block, hcl.Diagnostics) {
  1063  		content, _, diags := body.PartialContent(&hcl.BodySchema{
  1064  			Blocks: []hcl.BlockHeaderSchema{{Type: "block"}},
  1065  		})
  1066  		if diags.HasErrors() {
  1067  			return nil, diags
  1068  		}
  1069  		content, _, diags = content.Blocks[0].Body.PartialContent(&hcl.BodySchema{
  1070  			Blocks: []hcl.BlockHeaderSchema{{Type: "nested"}},
  1071  		})
  1072  		if diags.HasErrors() {
  1073  			return nil, diags
  1074  		}
  1075  		return content.Blocks[0], nil
  1076  	}
  1077  
  1078  	tests := []struct {
  1079  		name     string
  1080  		source   string
  1081  		getBlock func(hcl.Body) (*hcl.Block, hcl.Diagnostics)
  1082  		want     string
  1083  		errCheck func(error) bool
  1084  	}{
  1085  		{
  1086  			name:     "remove inline block",
  1087  			source:   `block { foo = 1 }`,
  1088  			getBlock: getFirstBlock,
  1089  			want:     ``,
  1090  			errCheck: neverHappend,
  1091  		},
  1092  		{
  1093  			name: "remove block",
  1094  			source: `
  1095  block {
  1096    foo = 1
  1097  }`,
  1098  			getBlock: getFirstBlock,
  1099  			want: `
  1100  `,
  1101  			errCheck: neverHappend,
  1102  		},
  1103  		{
  1104  			name: "remove block with comment",
  1105  			source: `
  1106  # comment
  1107  block {
  1108    foo = 1
  1109  }`,
  1110  			getBlock: getFirstBlock,
  1111  			want: `
  1112  `,
  1113  			errCheck: neverHappend,
  1114  		},
  1115  		{
  1116  			name: "remove block with multiple comments",
  1117  			source: `
  1118  # comment
  1119  # comment
  1120  block {
  1121    foo = 1
  1122  }`,
  1123  			getBlock: getFirstBlock,
  1124  			want: `
  1125  `,
  1126  			errCheck: neverHappend,
  1127  		},
  1128  		{
  1129  			name: "remove block with multi-line comment",
  1130  			source: `
  1131  /* comment */
  1132  block {
  1133    foo = 1
  1134  }`,
  1135  			getBlock: getFirstBlock,
  1136  			// This is the same behavior as hclwrite.RemoveBlock.
  1137  			want: `
  1138  /* comment */
  1139  `,
  1140  			errCheck: neverHappend,
  1141  		},
  1142  		{
  1143  			name: "remove block after attribute",
  1144  			source: `
  1145  bar = 1
  1146  block {
  1147    foo = 1
  1148  }`,
  1149  			getBlock: getFirstBlock,
  1150  			want: `
  1151  bar = 1
  1152  `,
  1153  			errCheck: neverHappend,
  1154  		},
  1155  		{
  1156  			name: "remove block after attribute and newline",
  1157  			source: `
  1158  bar = 1
  1159  
  1160  block {
  1161    foo = 1
  1162  }`,
  1163  			getBlock: getFirstBlock,
  1164  			want: `
  1165  bar = 1
  1166  
  1167  `,
  1168  			errCheck: neverHappend,
  1169  		},
  1170  		{
  1171  			name: "remove block after attribute with trailing comment",
  1172  			source: `
  1173  bar = 1 # comment
  1174  block {
  1175    foo = 1
  1176  }`,
  1177  			getBlock: getFirstBlock,
  1178  			want: `
  1179  bar = 1 # comment
  1180  `,
  1181  			errCheck: neverHappend,
  1182  		},
  1183  		{
  1184  			name: "remove inline block after attribute",
  1185  			source: `
  1186  bar = 1
  1187  block { foo = 1 }`,
  1188  			getBlock: getFirstBlock,
  1189  			want: `
  1190  bar = 1
  1191  `,
  1192  			errCheck: neverHappend,
  1193  		},
  1194  		{
  1195  			name: "remove nested block",
  1196  			source: `
  1197  block {
  1198    nested {
  1199      foo = 1
  1200    }
  1201  }`,
  1202  			getBlock: getNestedBlock,
  1203  			want: `
  1204  block {
  1205  }`,
  1206  			errCheck: neverHappend,
  1207  		},
  1208  		{
  1209  			name: "remove nested inline block",
  1210  			source: `
  1211  block {
  1212    nested { foo = 1 }
  1213  }`,
  1214  			getBlock: getNestedBlock,
  1215  			want: `
  1216  block {
  1217  }`,
  1218  			errCheck: neverHappend,
  1219  		},
  1220  		{
  1221  			name: "remove block with traling comment",
  1222  			source: `
  1223  block {
  1224    foo = 1
  1225  } # comment`,
  1226  			getBlock: getFirstBlock,
  1227  			want: `
  1228  `,
  1229  			errCheck: neverHappend,
  1230  		},
  1231  		{
  1232  			name: "remove block with next line comment",
  1233  			source: `
  1234  block {
  1235    foo = 1
  1236  }
  1237  # comment`,
  1238  			getBlock: getFirstBlock,
  1239  			want: `
  1240  # comment`,
  1241  			errCheck: neverHappend,
  1242  		},
  1243  		{
  1244  			name: "remove block in the middle",
  1245  			source: `
  1246  foo = 1
  1247  
  1248  block {
  1249    foo = 1
  1250  }
  1251  
  1252  baz = 1`,
  1253  			getBlock: getFirstBlock,
  1254  			want: `
  1255  foo = 1
  1256  
  1257  baz = 1`,
  1258  			errCheck: neverHappend,
  1259  		},
  1260  	}
  1261  
  1262  	for _, test := range tests {
  1263  		t.Run(test.name, func(t *testing.T) {
  1264  			file, diags := hclsyntax.ParseConfig([]byte(test.source), "main.tf", hcl.InitialPos)
  1265  			if diags.HasErrors() {
  1266  				t.Fatalf("failed to parse HCL: %s", diags)
  1267  			}
  1268  			block, diags := test.getBlock(file.Body)
  1269  			if diags.HasErrors() {
  1270  				t.Fatalf("failed to get block: %s", diags)
  1271  			}
  1272  
  1273  			fixer := NewFixer(map[string][]byte{"main.tf": []byte(test.source)})
  1274  
  1275  			err := fixer.RemoveBlock(block)
  1276  			if test.errCheck(err) {
  1277  				t.Fatalf("failed to check error: %s", err)
  1278  			}
  1279  
  1280  			if diff := cmp.Diff(test.want, string(fixer.changes["main.tf"])); diff != "" {
  1281  				t.Errorf(diff)
  1282  			}
  1283  		})
  1284  	}
  1285  }
  1286  
  1287  func TestRemoveExtBlock(t *testing.T) {
  1288  	// default error check helper
  1289  	neverHappend := func(err error) bool { return err != nil }
  1290  	// getFirstBlock returns the first block in the given body.
  1291  	getFirstBlock := func(body hcl.Body) (*hclext.Block, hcl.Diagnostics) {
  1292  		content, diags := hclext.PartialContent(body, &hclext.BodySchema{
  1293  			Blocks: []hclext.BlockSchema{{Type: "block"}},
  1294  		})
  1295  		if diags.HasErrors() {
  1296  			return nil, diags
  1297  		}
  1298  		return content.Blocks[0], nil
  1299  	}
  1300  
  1301  	tests := []struct {
  1302  		name     string
  1303  		source   string
  1304  		getBlock func(hcl.Body) (*hclext.Block, hcl.Diagnostics)
  1305  		want     string
  1306  		errCheck func(error) bool
  1307  	}{
  1308  		{
  1309  			name: "remove block",
  1310  			source: `
  1311  block {
  1312    foo = 1
  1313  }`,
  1314  			getBlock: getFirstBlock,
  1315  			want: `
  1316  `,
  1317  			errCheck: neverHappend,
  1318  		},
  1319  	}
  1320  
  1321  	for _, test := range tests {
  1322  		t.Run(test.name, func(t *testing.T) {
  1323  			file, diags := hclsyntax.ParseConfig([]byte(test.source), "main.tf", hcl.InitialPos)
  1324  			if diags.HasErrors() {
  1325  				t.Fatalf("failed to parse HCL: %s", diags)
  1326  			}
  1327  			block, diags := test.getBlock(file.Body)
  1328  			if diags.HasErrors() {
  1329  				t.Fatalf("failed to get block: %s", diags)
  1330  			}
  1331  
  1332  			fixer := NewFixer(map[string][]byte{"main.tf": []byte(test.source)})
  1333  
  1334  			err := fixer.RemoveExtBlock(block)
  1335  			if test.errCheck(err) {
  1336  				t.Fatalf("failed to check error: %s", err)
  1337  			}
  1338  
  1339  			if diff := cmp.Diff(test.want, string(fixer.changes["main.tf"])); diff != "" {
  1340  				t.Errorf(diff)
  1341  			}
  1342  		})
  1343  	}
  1344  }
  1345  
  1346  func TestTextAt(t *testing.T) {
  1347  	tests := []struct {
  1348  		name string
  1349  		src  map[string][]byte
  1350  		rng  hcl.Range
  1351  		want tflint.TextNode
  1352  	}{
  1353  		{
  1354  			name: "exists",
  1355  			src: map[string][]byte{
  1356  				"main.tf": []byte(`foo bar baz`),
  1357  			},
  1358  			rng: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 4}, End: hcl.Pos{Byte: 7}},
  1359  			want: tflint.TextNode{
  1360  				Bytes: []byte("bar"),
  1361  				Range: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 4}, End: hcl.Pos{Byte: 7}},
  1362  			},
  1363  		},
  1364  		{
  1365  			name: "does not exists",
  1366  			src: map[string][]byte{
  1367  				"main.tf": []byte(`foo bar baz`),
  1368  			},
  1369  			rng: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 14}, End: hcl.Pos{Byte: 17}},
  1370  			want: tflint.TextNode{
  1371  				Range: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 14}, End: hcl.Pos{Byte: 17}},
  1372  			},
  1373  		},
  1374  	}
  1375  
  1376  	for _, test := range tests {
  1377  		t.Run(test.name, func(t *testing.T) {
  1378  			fixer := NewFixer(test.src)
  1379  			got := fixer.TextAt(test.rng)
  1380  			if diff := cmp.Diff(test.want, got); diff != "" {
  1381  				t.Errorf(diff)
  1382  			}
  1383  		})
  1384  	}
  1385  }
  1386  
  1387  // @see https://github.com/hashicorp/hcl/blob/v2.17.0/hclwrite/generate_test.go#L18
  1388  func TestValueText(t *testing.T) {
  1389  	tests := []struct {
  1390  		value cty.Value
  1391  		want  string
  1392  	}{
  1393  		{
  1394  			value: cty.NullVal(cty.DynamicPseudoType),
  1395  			want:  "null",
  1396  		},
  1397  		{
  1398  			value: cty.True,
  1399  			want:  "true",
  1400  		},
  1401  		{
  1402  			value: cty.False,
  1403  			want:  "false",
  1404  		},
  1405  		{
  1406  			value: cty.NumberIntVal(0),
  1407  			want:  "0",
  1408  		},
  1409  		{
  1410  			value: cty.NumberFloatVal(0.5),
  1411  			want:  "0.5",
  1412  		},
  1413  		{
  1414  			value: cty.NumberVal(big.NewFloat(0).SetPrec(512).Mul(big.NewFloat(40000000), big.NewFloat(2000000))),
  1415  			want:  "80000000000000",
  1416  		},
  1417  		{
  1418  			value: cty.StringVal(""),
  1419  			want:  `""`,
  1420  		},
  1421  		{
  1422  			value: cty.StringVal("foo"),
  1423  			want:  `"foo"`,
  1424  		},
  1425  		{
  1426  			value: cty.StringVal(`"foo"`),
  1427  			want:  `"\"foo\""`,
  1428  		},
  1429  		{
  1430  			value: cty.StringVal("hello\nworld\n"),
  1431  			want:  `"hello\nworld\n"`,
  1432  		},
  1433  		{
  1434  			value: cty.StringVal("hello\r\nworld\r\n"),
  1435  			want:  `"hello\r\nworld\r\n"`,
  1436  		},
  1437  		{
  1438  			value: cty.StringVal(`what\what`),
  1439  			want:  `"what\\what"`,
  1440  		},
  1441  		{
  1442  			value: cty.StringVal("𝄞"),
  1443  			want:  `"𝄞"`,
  1444  		},
  1445  		{
  1446  			value: cty.StringVal("👩🏾"),
  1447  			want:  `"👩🏾"`,
  1448  		},
  1449  		{
  1450  			value: cty.EmptyTupleVal,
  1451  			want:  "[]",
  1452  		},
  1453  		{
  1454  			value: cty.TupleVal([]cty.Value{cty.EmptyTupleVal}),
  1455  			want:  "[[]]",
  1456  		},
  1457  		{
  1458  			value: cty.ListValEmpty(cty.String),
  1459  			want:  "[]",
  1460  		},
  1461  		{
  1462  			value: cty.SetValEmpty(cty.Bool),
  1463  			want:  "[]",
  1464  		},
  1465  		{
  1466  			value: cty.TupleVal([]cty.Value{cty.True}),
  1467  			want:  "[true]",
  1468  		},
  1469  		{
  1470  			value: cty.TupleVal([]cty.Value{cty.True, cty.NumberIntVal(0)}),
  1471  			want:  "[true, 0]",
  1472  		},
  1473  		{
  1474  			value: cty.EmptyObjectVal,
  1475  			want:  "{}",
  1476  		},
  1477  		{
  1478  			value: cty.MapValEmpty(cty.Bool),
  1479  			want:  "{}",
  1480  		},
  1481  		{
  1482  			value: cty.ObjectVal(map[string]cty.Value{
  1483  				"foo": cty.True,
  1484  			}),
  1485  			want: "{ foo = true }",
  1486  		},
  1487  		{
  1488  			value: cty.ObjectVal(map[string]cty.Value{
  1489  				"foo": cty.True,
  1490  				"bar": cty.NumberIntVal(0),
  1491  			}),
  1492  			want: "{ bar = 0, foo = true }",
  1493  		},
  1494  		{
  1495  			value: cty.ObjectVal(map[string]cty.Value{
  1496  				"foo bar": cty.True,
  1497  			}),
  1498  			want: `{ "foo bar" = true }`,
  1499  		},
  1500  	}
  1501  
  1502  	for _, test := range tests {
  1503  		t.Run(test.value.GoString(), func(t *testing.T) {
  1504  			fixer := NewFixer(nil)
  1505  			got := fixer.ValueText(test.value)
  1506  			if diff := cmp.Diff(test.want, got); diff != "" {
  1507  				t.Errorf(diff)
  1508  			}
  1509  		})
  1510  	}
  1511  }
  1512  
  1513  func TestRangeTo(t *testing.T) {
  1514  	start := hcl.Pos{Byte: 10, Line: 2, Column: 1}
  1515  
  1516  	tests := []struct {
  1517  		name string
  1518  		to   string
  1519  		want hcl.Range
  1520  	}{
  1521  		{
  1522  			name: "empty",
  1523  			to:   "",
  1524  			want: hcl.Range{Start: start, End: start},
  1525  		},
  1526  		{
  1527  			name: "single line",
  1528  			to:   "foo",
  1529  			want: hcl.Range{Start: start, End: hcl.Pos{Byte: 13, Line: 2, Column: 4}},
  1530  		},
  1531  		{
  1532  			name: "trailing new line",
  1533  			to:   "foo\n",
  1534  			want: hcl.Range{Start: start, End: hcl.Pos{Byte: 13, Line: 2, Column: 4}},
  1535  		},
  1536  		{
  1537  			name: "multi new line",
  1538  			to:   "foo\nbar",
  1539  			want: hcl.Range{Start: start, End: hcl.Pos{Byte: 17, Line: 3, Column: 4}},
  1540  		},
  1541  		{
  1542  			name: "multibytes",
  1543  			to:   "こんにちは世界",
  1544  			want: hcl.Range{Start: start, End: hcl.Pos{Byte: 31, Line: 2, Column: 8}},
  1545  		},
  1546  	}
  1547  
  1548  	for _, test := range tests {
  1549  		t.Run(test.name, func(t *testing.T) {
  1550  			fixer := NewFixer(nil)
  1551  
  1552  			got := fixer.RangeTo(test.to, "", start)
  1553  			if diff := cmp.Diff(test.want, got); diff != "" {
  1554  				t.Errorf(diff)
  1555  			}
  1556  		})
  1557  	}
  1558  }
  1559  
  1560  func TestChanges(t *testing.T) {
  1561  	src := map[string][]byte{
  1562  		"main.tf": []byte(`
  1563  foo = 1
  1564    bar = 2
  1565  `),
  1566  		"main.tf.json": []byte(`{"foo": 1, "bar": 2}`),
  1567  	}
  1568  	fixer := NewFixer(src)
  1569  
  1570  	if len(fixer.Changes()) != 0 {
  1571  		t.Errorf("unexpected changes: %#v", fixer.Changes())
  1572  	}
  1573  	if fixer.HasChanges() {
  1574  		t.Errorf("unexpected changes: %#v", fixer.Changes())
  1575  	}
  1576  	if len(fixer.shifts) != 0 {
  1577  		t.Errorf("unexpected shifts: %#v", fixer.shifts)
  1578  	}
  1579  
  1580  	// Make changes
  1581  	if err := fixer.ReplaceText(
  1582  		hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 11}, End: hcl.Pos{Byte: 14}},
  1583  		"barbaz",
  1584  	); err != nil {
  1585  		t.Fatal(err)
  1586  	}
  1587  	if err := fixer.ReplaceText(
  1588  		hcl.Range{Filename: "main.tf.json", Start: hcl.Pos{Byte: 12}, End: hcl.Pos{Byte: 15}},
  1589  		"barbaz",
  1590  	); err != nil {
  1591  		t.Fatal(err)
  1592  	}
  1593  
  1594  	changed := map[string][]byte{
  1595  		"main.tf": []byte(`
  1596  foo = 1
  1597    barbaz = 2
  1598  `),
  1599  		"main.tf.json": []byte(`{"foo": 1, "barbaz": 2}`),
  1600  	}
  1601  
  1602  	if diff := cmp.Diff(src, fixer.sources); diff != "" {
  1603  		t.Errorf(diff)
  1604  	}
  1605  	if diff := cmp.Diff(fixer.Changes(), changed); diff != "" {
  1606  		t.Errorf(diff)
  1607  	}
  1608  	if !fixer.HasChanges() {
  1609  		t.Errorf("unexpected changes: %#v", fixer.Changes())
  1610  	}
  1611  	if len(fixer.shifts) != 2 {
  1612  		t.Errorf("unexpected shifts: %#v", fixer.shifts)
  1613  	}
  1614  
  1615  	// Format changes
  1616  	fixer.FormatChanges()
  1617  
  1618  	fixed := map[string][]byte{
  1619  		"main.tf": []byte(`
  1620  foo    = 1
  1621  barbaz = 2
  1622  `),
  1623  		"main.tf.json": []byte(`{"foo": 1, "barbaz": 2}`),
  1624  	}
  1625  
  1626  	if diff := cmp.Diff(fixer.Changes(), fixed); diff != "" {
  1627  		t.Errorf(diff)
  1628  	}
  1629  	if len(fixer.shifts) != 2 {
  1630  		t.Errorf("unexpected shifts: %#v", fixer.shifts)
  1631  	}
  1632  
  1633  	// Apply changes
  1634  	fixer.ApplyChanges()
  1635  
  1636  	if diff := cmp.Diff(fixed, fixer.sources); diff != "" {
  1637  		t.Errorf(diff)
  1638  	}
  1639  	if len(fixer.Changes()) != 0 {
  1640  		t.Errorf("unexpected changes: %#v", fixer.Changes())
  1641  	}
  1642  	if fixer.HasChanges() {
  1643  		t.Errorf("unexpected changes: %#v", fixer.Changes())
  1644  	}
  1645  	if len(fixer.shifts) != 0 {
  1646  		t.Errorf("unexpected shifts: %#v", fixer.shifts)
  1647  	}
  1648  }
  1649  
  1650  func TestStashChanges(t *testing.T) {
  1651  	tests := []struct {
  1652  		name   string
  1653  		source string
  1654  		fix    func(*Fixer) error
  1655  		want   string
  1656  		shifts int
  1657  	}{
  1658  		{
  1659  			name:   "no changes",
  1660  			source: `foo`,
  1661  			fix: func(fixer *Fixer) error {
  1662  				fixer.StashChanges()
  1663  				fixer.PopChangesFromStash()
  1664  				return nil
  1665  			},
  1666  			want:   "",
  1667  			shifts: 0,
  1668  		},
  1669  		{
  1670  			name:   "changes after stash",
  1671  			source: `foo`,
  1672  			fix: func(fixer *Fixer) error {
  1673  				fixer.StashChanges()
  1674  				if err := fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 0}, End: hcl.Pos{Byte: 0}}, "bar"); err != nil {
  1675  					return err
  1676  				}
  1677  				fixer.PopChangesFromStash()
  1678  				return nil
  1679  			},
  1680  			want:   "",
  1681  			shifts: 0,
  1682  		},
  1683  		{
  1684  			name:   "stash after changes",
  1685  			source: `foo`,
  1686  			fix: func(fixer *Fixer) error {
  1687  				if err := fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 0}, End: hcl.Pos{Byte: 0}}, "bar"); err != nil {
  1688  					return err
  1689  				}
  1690  				fixer.StashChanges()
  1691  				if err := fixer.ReplaceText(hcl.Range{Filename: "main.tf", Start: hcl.Pos{Byte: 0}, End: hcl.Pos{Byte: 0}}, "baz"); err != nil {
  1692  					return err
  1693  				}
  1694  				fixer.PopChangesFromStash()
  1695  				return nil
  1696  			},
  1697  			want:   "barfoo",
  1698  			shifts: 1,
  1699  		},
  1700  	}
  1701  
  1702  	for _, test := range tests {
  1703  		t.Run(test.name, func(t *testing.T) {
  1704  			fixer := NewFixer(map[string][]byte{"main.tf": []byte(test.source)})
  1705  			if err := test.fix(fixer); err != nil {
  1706  				t.Fatalf("failed to fix: %s", err)
  1707  			}
  1708  
  1709  			if diff := cmp.Diff(test.want, string(fixer.changes["main.tf"])); diff != "" {
  1710  				t.Errorf(diff)
  1711  			}
  1712  			if test.shifts != len(fixer.shifts) {
  1713  				t.Errorf("shifts: want %d, got %d", test.shifts, len(fixer.shifts))
  1714  			}
  1715  		})
  1716  	}
  1717  }