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 }