├── .github ├── dependabot.yml └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── main.go ├── main_test.go ├── modifytags ├── modifytags.go ├── modifytags_test.go └── test-fixtures │ ├── add_tags_pos_between_line.golden │ ├── add_tags_pos_between_line.input │ ├── all_structs.golden │ ├── all_structs.input │ ├── clear_all_tags.golden │ ├── clear_all_tags.input │ ├── remove_some_tags.golden │ └── remove_some_tags.input └── test-fixtures ├── all_structs.golden ├── all_structs.input ├── empty_file.golden ├── empty_file.input ├── errors.golden ├── errors.input ├── field_add.golden ├── field_add.input ├── field_add_existing.golden ├── field_add_existing.input ├── field_add_same_line.golden ├── field_add_same_line.input ├── field_clear_options.golden ├── field_clear_options.input ├── field_clear_tags.golden ├── field_clear_tags.input ├── field_remove.golden ├── field_remove.input ├── json_all_structs.golden ├── json_all_structs.input ├── json_errors.golden ├── json_errors.input ├── json_full.golden ├── json_full.input ├── json_intersection.golden ├── json_intersection.input ├── json_not_formatted.golden ├── json_not_formatted.input ├── json_not_formatted_2.golden ├── json_not_formatted_2.input ├── json_not_formatted_3.golden ├── json_not_formatted_3.input ├── json_not_formatted_4.golden ├── json_not_formatted_4.input ├── json_not_formatted_5.golden ├── json_not_formatted_5.input ├── json_not_formatted_6.golden ├── json_not_formatted_6.input ├── json_single.golden ├── json_single.input ├── line_add.golden ├── line_add.input ├── line_add_comment.golden ├── line_add_comment.input ├── line_add_intersect_partial.golden ├── line_add_intersect_partial.input ├── line_add_multiple_option.golden ├── line_add_multiple_option.input ├── line_add_no_override.golden ├── line_add_no_override.input ├── line_add_option.golden ├── line_add_option.input ├── line_add_option_existing.golden ├── line_add_option_existing.input ├── line_add_option_with_equal.golden ├── line_add_option_with_equal.input ├── line_add_outside.golden ├── line_add_outside.input ├── line_add_outside_partial_end.golden ├── line_add_outside_partial_end.input ├── line_add_outside_partial_start.golden ├── line_add_outside_partial_start.input ├── line_add_override.golden ├── line_add_override.input ├── line_add_override_column.golden ├── line_add_override_column.input ├── line_add_override_mixed_column_and_equal.golden ├── line_add_override_mixed_column_and_equal.input ├── line_add_override_multi_column.golden ├── line_add_override_multi_column.input ├── line_add_override_multi_equal.golden ├── line_add_override_multi_equal.input ├── line_camelcase_add.golden ├── line_camelcase_add.input ├── line_camelcase_add_embedded.golden ├── line_camelcase_add_embedded.input ├── line_directive_unix.golden ├── line_directive_unix.input ├── line_directive_windows.golden ├── line_directive_windows.input ├── line_lispcase_add.golden ├── line_lispcase_add.input ├── line_multiple_add.golden ├── line_multiple_add.input ├── line_pascalcase_add.golden ├── line_pascalcase_add.input ├── line_pascalcase_add_embedded.golden ├── line_pascalcase_add_embedded.input ├── line_remove.golden ├── line_remove.input ├── line_remove_option.golden ├── line_remove_option.input ├── line_remove_option_with_equal.golden ├── line_remove_option_with_equal.input ├── line_remove_options.golden ├── line_remove_options.input ├── line_titlecase_add.golden ├── line_titlecase_add.input ├── line_titlecase_add_embedded.golden ├── line_titlecase_add_embedded.input ├── line_value_add.golden ├── line_value_add.input ├── not_formatted.golden ├── not_formatted.input ├── offset_add.golden ├── offset_add.input ├── offset_add_composite.golden ├── offset_add_composite.input ├── offset_add_duplicate.golden ├── offset_add_duplicate.input ├── offset_add_literal_in.golden ├── offset_add_literal_in.input ├── offset_add_literal_out.golden ├── offset_add_literal_out.input ├── offset_anonymous_struct.golden ├── offset_anonymous_struct.input ├── offset_array_struct.golden ├── offset_array_struct.input ├── offset_star_struct.golden ├── offset_star_struct.input ├── skip_embedded.golden ├── skip_embedded.input ├── skip_private.golden ├── skip_private.input ├── skip_private_multiple_names.golden ├── skip_private_multiple_names.input ├── struct_add.golden ├── struct_add.input ├── struct_add_existing.golden ├── struct_add_existing.input ├── struct_add_underscore.golden ├── struct_add_underscore.input ├── struct_clear_options.golden ├── struct_clear_options.input ├── struct_clear_tags.golden ├── struct_clear_tags.input ├── struct_format.golden ├── struct_format.input ├── struct_format_existing.golden ├── struct_format_existing.input ├── struct_format_existing_oldstyle.golden ├── struct_format_existing_oldstyle.input ├── struct_format_oldstyle.golden ├── struct_format_oldstyle.input ├── struct_remove.golden └── struct_remove.input /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" # Location of package manifests 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | jobs: 9 | test: 10 | name: Test & Build 11 | runs-on: ubuntu-latest 12 | steps: 13 | 14 | - name: Set up Go 15 | uses: actions/setup-go@v5 16 | with: 17 | go-version: '>=1.22.0' 18 | 19 | - name: Check out code into the Go module directory 20 | uses: actions/checkout@v4 21 | 22 | - name: Run go mod tidy 23 | run: | 24 | set -e 25 | go mod tidy 26 | output=$(git status -s) 27 | if [ -z "${output}" ]; then 28 | exit 0 29 | fi 30 | echo 'We wish to maintain a tidy state for go mod. Please run `go mod tidy` on your branch, commit and push again.' 31 | echo 'Running `go mod tidy` on this CI test yields with the following changes:' 32 | echo "$output" 33 | exit 1 34 | 35 | - name: Test 36 | run: go test -race ./... 37 | 38 | - name: Lint 39 | run: "go vet ./..." 40 | 41 | - name: Staticcheck 42 | uses: dominikh/staticcheck-action@v1.3.0 43 | with: 44 | version: "2025.1" 45 | install-go: false 46 | 47 | - name: Build 48 | run: go build ./... 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Fatih Arslan 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of gomodifytags nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gomodifytags [![](https://github.com/fatih/gomodifytags/workflows/build/badge.svg)](https://github.com/fatih/gomodifytags/actions) 2 | 3 | Go tool to modify/update field tags in structs. `gomodifytags` makes it easy to 4 | update, add or delete the tags in a struct field. You can easily add new tags, 5 | update existing tags (such as appending a new key, i.e: `db`, `xml`, etc..) or 6 | remove existing tags. It also allows you to add and remove tag options. It's 7 | intended to be used by an editor, but also has modes to run it from the 8 | terminal. Read the usage section below for more information. 9 | 10 | ![gomodifytags](https://user-images.githubusercontent.com/438920/32691304-a1c7e47c-c716-11e7-977c-f4d0f8c616be.gif) 11 | 12 | 13 | # Install 14 | 15 | ```bash 16 | go install github.com/fatih/gomodifytags@latest 17 | ``` 18 | 19 | # Supported editors 20 | 21 | * [vim-go](https://github.com/fatih/vim-go) with `:GoAddTags` and `:GoRemoveTags` 22 | * [go-plus (atom)](https://github.com/joefitzgerald/go-plus) with commands `golang:add-tags` and `golang:remove-tags` 23 | * [vscode-go](https://github.com/golang/vscode-go) with commands `Go: Add Tags` and `Go: Remove Tags` 24 | * [A (Acme)](https://github.com/davidrjenni/A) with commands `addtags` and `rmtags` 25 | * [emacs-go-tag](https://github.com/brantou/emacs-go-tag) with commands `go-tag-add` and `go-tag-remove` 26 | * [TextMate2](https://github.com/vigo/textmate2-gomodifytags) 27 | 28 | # Usage 29 | 30 | `gomodifytags` has multiple ways to modify a tag. Let's start with an example package: 31 | 32 | ```go 33 | package main 34 | 35 | type Server struct { 36 | Name string 37 | Port int 38 | EnableLogs bool 39 | BaseDomain string 40 | Credentials struct { 41 | Username string 42 | Password string 43 | } 44 | } 45 | ``` 46 | 47 | We have to first pass a file. For that we can use the `-file` flag: 48 | 49 | ```sh 50 | $ gomodifytags -file demo.go 51 | -line, -offset, -struct or -all is not passed 52 | ``` 53 | 54 | What are these? There are four different ways of defining **which** field tags 55 | to change: 56 | 57 | * `-struct`: This accepts the struct name. i.e: `-struct Server`. The name 58 | should be a valid type name. The `-struct` flag selects the whole struct, and 59 | thus it will operate on all fields. 60 | * `-field`: This accepts a field name. i.e: `-field Address`. Useful to select 61 | a certain field. The name should be a valid field name. The `-struct` flag is required. 62 | * `-offset`: This accepts a byte offset of the file. Useful for editors to pass 63 | the position under the cursor. i.e: `-offset 548`. The offset has to be 64 | inside a valid struct. The `-offset` selects the whole struct. If you need 65 | more granular option see `-line` 66 | * `-line`: This accepts a string that defines the line or lines of which fields 67 | should be changed. I.e: `-line 4` or `-line 5,8` 68 | * `-all`: This is a boolean. The `-all` flag selects all structs of the given file. 69 | 70 | Let's continue by using the `-struct` tag: 71 | 72 | ``` 73 | $ gomodifytags -file demo.go -struct Server 74 | one of [-add-tags, -add-options, -remove-tags, -remove-options, -clear-tags, -clear-options] should be defined 75 | ``` 76 | 77 | ## Adding tags & options 78 | 79 | There are many options on how you can change the struct. Let us start by adding 80 | tags. The following will add the `json` key to all fields. The value will be 81 | automatically inherited from the field name and transformed to `snake_case`: 82 | 83 | ``` 84 | $ gomodifytags -file demo.go -struct Server -add-tags json 85 | ``` 86 | ```go 87 | package main 88 | 89 | type Server struct { 90 | Name string `json:"name"` 91 | Port int `json:"port"` 92 | EnableLogs bool `json:"enable_logs"` 93 | BaseDomain string `json:"base_domain"` 94 | Credentials struct { 95 | Username string `json:"username"` 96 | Password string `json:"password"` 97 | } `json:"credentials"` 98 | } 99 | ``` 100 | 101 | By default changes will be printed to stdout and can be used for dry-run your 102 | changes before making destructive changes. If you want to change it permanently, 103 | pass the `-w` (write) flag. 104 | 105 | ``` 106 | $ gomodifytags -file demo.go -struct Server -add-tags json -w 107 | ``` 108 | 109 | You can disable printing the results to stdout with the `--quiet` flag: 110 | 111 | ``` 112 | $ gomodifytags -file demo.go -struct Server -add-tags json -w --quiet 113 | ``` 114 | 115 | You can pass multiple keys to add tags. The following will add `json` and `xml` 116 | keys: 117 | 118 | ``` 119 | $ gomodifytags -file demo.go -struct Server -add-tags json,xml 120 | ``` 121 | ```go 122 | package main 123 | 124 | type Server struct { 125 | Name string `json:"name" xml:"name"` 126 | Port int `json:"port" xml:"port"` 127 | EnableLogs bool `json:"enable_logs" xml:"enable_logs"` 128 | BaseDomain string `json:"base_domain" xml:"base_domain"` 129 | Credentials struct { 130 | Username string `json:"username" xml:"username"` 131 | Password string `json:"password" xml:"password"` 132 | } `json:"credentials" xml:"credentials"` 133 | } 134 | ``` 135 | 136 | The default transformation is `snake_case` when using the gomodifytags command 137 | and when calling Apply() - see section below for more information about Apply(). 138 | 139 | If you prefer to use `camelCase` instead of `snake_case` for the values, you 140 | can use the `-transform` flag to define a different transformation rule. The 141 | following example uses the `camelcase` transformation rule: 142 | 143 | 144 | ``` 145 | $ gomodifytags -file demo.go -struct Server -add-tags json,xml -transform camelcase 146 | ``` 147 | ```go 148 | package main 149 | 150 | type Server struct { 151 | Name string `json:"name" xml:"name"` 152 | Port int `json:"port" xml:"port"` 153 | EnableLogs bool `json:"enableLogs" xml:"enableLogs"` 154 | BaseDomain string `json:"baseDomain" xml:"baseDomain"` 155 | Credentials struct { 156 | Username string `json:"username" xml:"username"` 157 | Password string `json:"password" xml:"password"` 158 | } `json:"credentials" xml:"credentials"` 159 | } 160 | ``` 161 | 162 | ### Formatting tag values 163 | 164 | By default a struct tag's value is transformed from a struct's field and used 165 | directly. As an example for the field `Server string`, we generate a tag in the 166 | form: `json:"server"` (assuming `-add-tags=json` is used). 167 | 168 | However, some third party libraries use tags in a different way and might 169 | require to them to have a particular formatting, such as is the case of 170 | prefixing them (`field_name=`). The `--template` flag allows you to 171 | specify a custom format for the tag value to be applied. 172 | 173 | ``` 174 | $ gomodifytags -file demo.go -struct Server -add-tags gaum -template "field_name={field}" 175 | ``` 176 | 177 | ```go 178 | package main 179 | 180 | type Server struct { 181 | Name string `gaum:"field_name=name"` 182 | Port int `gaum:"field_name=port"` 183 | EnableLogs bool `gaum:"field_name=enableLogs"` 184 | BaseDomain string `gaum:"field_name=baseDomain"` 185 | } 186 | ``` 187 | 188 | The `{field}` word is a special keyword that is replaced by the struct tag's value 189 | **after** the [transformation](https://github.com/fatih/gomodifytags#transformations). 190 | 191 | ### Transformations 192 | 193 | We currently support the following transformations: 194 | 195 | * `snakecase`: `"BaseDomain"` -> `"base_domain"` 196 | * `camelcase`: `"BaseDomain"` -> `"baseDomain"` 197 | * `lispcase`: `"BaseDomain"` -> `"base-domain"` 198 | * `pascalcase`: `"BaseDomain"` -> `"BaseDomain"` 199 | * `titlecase`: `"BaseDomain"` -> `"Base Domain"` 200 | * `keep`: keeps the original field name 201 | 202 | You can also pass a static value for each fields. This is useful if you use Go 203 | packages that validates the struct fields or extract values for certain 204 | operations. The following example adds the `json` key, a `validate` key with 205 | the value set to `gt=1` and the `scope` key with the value `read-only`: 206 | 207 | ``` 208 | $ gomodifytags -file demo.go -struct Server -add-tags json,validate:gt=1,scope:read-only 209 | ``` 210 | ```go 211 | package main 212 | 213 | type Server struct { 214 | Name string `json:"name" validate:"gt=1" scope:"read-only"` 215 | Port int `json:"port" validate:"gt=1" scope:"read-only"` 216 | EnableLogs bool `json:"enable_logs" validate:"gt=1" scope:"read-only"` 217 | BaseDomain string `json:"base_domain" validate:"gt=1" scope:"read-only"` 218 | Credentials struct { 219 | Username string `json:"username" validate:"gt=1" scope:"read-only"` 220 | Password string `json:"password" validate:"gt=1" scope:"read-only"` 221 | } `json:"credentials" validate:"gt=1" scope:"read-only"` 222 | } 223 | ``` 224 | 225 | To add `options` to for a given key, we use the `-add-options` flag. In the 226 | example below we're going to add the `json` key and the `omitempty` option to 227 | all json keys: 228 | 229 | ``` 230 | $ gomodifytags -file demo.go -struct Server -add-tags json -add-options json=omitempty 231 | ``` 232 | ```go 233 | package main 234 | 235 | type Server struct { 236 | Name string `json:"name,omitempty"` 237 | Port int `json:"port,omitempty"` 238 | EnableLogs bool `json:"enable_logs,omitempty"` 239 | BaseDomain string `json:"base_domain,omitempty"` 240 | Credentials struct { 241 | Username string `json:"username,omitempty"` 242 | Password string `json:"password,omitempty"` 243 | } `json:"credentials,omitempty"` 244 | } 245 | ``` 246 | 247 | If the key already exists you don't have to use `-add-tags` 248 | 249 | 250 | ### Skipping unexported fields 251 | 252 | By default all fields are processed. This main reason for this is to allow 253 | structs to evolve with time and be ready in case a field is exported in the 254 | future. However if you don't like this behavior, you can skip it by passing the 255 | `--skip-unexported` flag: 256 | 257 | ``` 258 | $ gomodifytags -file demo.go -struct Server -add-tags json --skip-unexported 259 | ``` 260 | ```go 261 | package main 262 | 263 | type Server struct { 264 | Name string `json:"name"` 265 | Port int `json:"port"` 266 | enableLogs bool 267 | baseDomain string 268 | } 269 | ``` 270 | 271 | ## Removing tags & options 272 | 273 | Let's continue with removing tags. We're going to use the following simple package: 274 | 275 | ```go 276 | package main 277 | 278 | type Server struct { 279 | Name string `json:"name,omitempty" xml:"name,attr,cdata"` 280 | Port int `json:"port,omitempty" xml:"port,attr,cdata"` 281 | EnableLogs bool `json:"enable_logs,omitempty" xml:"enable_logs,attr,cdata"` 282 | BaseDomain string `json:"base_domain,omitempty" xml:"base_domain,attr,cdata"` 283 | Credentials struct { 284 | Username string `json:"username,omitempty" xml:"username,attr,cdata"` 285 | Password string `json:"password,omitempty" xml:"password,attr,cdata"` 286 | } `json:"credentials,omitempty" xml:"credentials,attr,cdata"` 287 | } 288 | ``` 289 | 290 | To remove the xml tags, we're going to use the `-remove-tags` flag: 291 | 292 | ``` 293 | $ gomodifytags -file demo.go -struct Server -remove-tags xml 294 | ``` 295 | ```go 296 | package main 297 | 298 | type Server struct { 299 | Name string `json:"name"` 300 | Port int `json:"port"` 301 | EnableLogs bool `json:"enable_logs"` 302 | BaseDomain string `json:"base_domain"` 303 | Credentials struct { 304 | Username string `json:"username"` 305 | Password string `json:"password"` 306 | } `json:"credentials"` 307 | } 308 | ``` 309 | 310 | You can also remove multiple tags. The example below removes `json` and `xml`: 311 | 312 | ``` 313 | $ gomodifytags -file demo.go -struct Server -remove-tags json,xml 314 | ``` 315 | ```go 316 | package main 317 | 318 | type Server struct { 319 | Name string 320 | Port int 321 | EnableLogs bool 322 | BaseDomain string 323 | Credentials struct { 324 | Username string 325 | Password string 326 | } 327 | } 328 | ``` 329 | 330 | If you want to remove all keys, we can also use the `-clear-tags` flag. This 331 | flag removes all tags and doesn't require to explicitly pass the key names: 332 | 333 | ``` 334 | $ gomodifytags -file demo.go -struct Server -clear-tags 335 | ``` 336 | ```go 337 | package main 338 | 339 | type Server struct { 340 | Name string 341 | Port int 342 | EnableLogs bool 343 | BaseDomain string 344 | Credentials struct { 345 | Username string 346 | Password string 347 | } 348 | } 349 | ``` 350 | 351 | To remove any option, we can use the `-remove-options` flag. The following will 352 | remove all `omitempty` flags from the `json` key: 353 | 354 | ``` 355 | $ gomodifytags -file demo.go -struct Server -remove-options json=omitempty 356 | ``` 357 | ```go 358 | package main 359 | 360 | type Server struct { 361 | Name string `json:"name" xml:"name,attr,cdata"` 362 | Port int `json:"port" xml:"port,attr,cdata"` 363 | EnableLogs bool `json:"enable_logs" xml:"enable_logs,attr,cdata"` 364 | BaseDomain string `json:"base_domain" xml:"base_domain,attr,cdata"` 365 | Credentials struct { 366 | Username string `json:"username" xml:"username,attr,cdata"` 367 | Password string `json:"password" xml:"password,attr,cdata"` 368 | } `json:"credentials" xml:"credentials,attr,cdata"` 369 | } 370 | ``` 371 | 372 | To remove multiple options from multiple tags just add another options: 373 | 374 | ``` 375 | $ gomodifytags -file demo.go -struct Server -remove-options json=omitempty,xml=cdata 376 | ``` 377 | ```go 378 | package main 379 | 380 | type Server struct { 381 | Name string `json:"name" xml:"name,attr"` 382 | Port int `json:"port" xml:"port,attr"` 383 | EnableLogs bool `json:"enable_logs" xml:"enable_logs,attr"` 384 | BaseDomain string `json:"base_domain" xml:"base_domain,attr"` 385 | Credentials struct { 386 | Username string `json:"username" xml:"username,attr"` 387 | Password string `json:"password" xml:"password,attr"` 388 | } `json:"credentials" xml:"credentials,attr"` 389 | } 390 | ``` 391 | 392 | To remove multiple options from the same tag, you can repeat the tag name. For example: 393 | 394 | ``` 395 | $ gomodifytags -file demo.go -struct Server -remove-options json=first_option,json=other_option 396 | ``` 397 | 398 | Lastly, to remove all options without explicitly defining the keys and names, 399 | we can use the `-clear-options` flag. The following example will remove all 400 | options for the given struct: 401 | 402 | ``` 403 | $ gomodifytags -file demo.go -struct Server -clear-options 404 | ``` 405 | ```go 406 | package main 407 | 408 | type Server struct { 409 | Name string `json:"name" xml:"name"` 410 | Port int `json:"port" xml:"port"` 411 | EnableLogs bool `json:"enable_logs" xml:"enable_logs"` 412 | BaseDomain string `json:"base_domain" xml:"base_domain"` 413 | Credentials struct { 414 | Username string `json:"username" xml:"username"` 415 | Password string `json:"password" xml:"password"` 416 | } `json:"credentials" xml:"credentials"` 417 | } 418 | ``` 419 | 420 | ## Line based modification 421 | 422 | So far all examples used the `-struct` flag. However we also can pass the line 423 | numbers to only change certain files. Suppose we only want to remove the tags 424 | for the `Credentials` struct (including the fields) for the following code (lines are included): 425 | 426 | ```go 427 | 01 package main 428 | 02 429 | 03 type Server struct { 430 | 04 Name string `json:"name" xml:"name"` 431 | 05 Port int `json:"port" xml:"port"` 432 | 06 EnableLogs bool `json:"enable_logs" xml:"enable_logs"` 433 | 07 BaseDomain string `json:"base_domain" xml:"base_domain"` 434 | 08 Credentials struct { 435 | 09 Username string `json:"username" xml:"username"` 436 | 10 Password string `json:"password" xml:"password"` 437 | 11 } `json:"credentials" xml:"credentials"` 438 | 12 } 439 | ``` 440 | 441 | To remove the tags for the credentials we're going to pass the `-line` flag: 442 | 443 | ``` 444 | $ gomodifytags -file demo.go -line 8,11 -clear-tags xml 445 | ``` 446 | ```go 447 | package main 448 | 449 | type Server struct { 450 | Name string `json:"name" xml:"name"` 451 | Port int `json:"port" xml:"port"` 452 | EnableLogs bool `json:"enable_logs" xml:"enable_logs"` 453 | BaseDomain string `json:"base_domain" xml:"base_domain"` 454 | Credentials struct { 455 | Username string 456 | Password string 457 | } 458 | } 459 | ``` 460 | 461 | For removing the xml tags for certain lines, we can use the `-remove-tags` 462 | field. The following example will remove the `xml` tags for the lines 6 and 7 463 | (fields with names of `EnableLogs` and `BaseDomain`): 464 | 465 | ``` 466 | $ gomodifytags -file demo.go -line 6,7 -remove-tags xml 467 | ``` 468 | ```go 469 | package main 470 | 471 | type Server struct { 472 | Name string `json:"name" xml:"name"` 473 | Port int `json:"port" xml:"port"` 474 | EnableLogs bool `json:"enable_logs"` 475 | BaseDomain string `json:"base_domain"` 476 | Credentials struct { 477 | Username string `json:"username" xml:"username"` 478 | Password string `json:"password" xml:"password"` 479 | } `json:"credentials" xml:"credentials"` 480 | } 481 | ``` 482 | 483 | The same logic applies to adding tags or any other option as well. To add the 484 | `bson` tag to the lines between 5 and 7, we can use the following example: 485 | 486 | ``` 487 | $ gomodifytags -file demo.go -line 5,7 -add-tags bson 488 | ``` 489 | ```go 490 | package main 491 | 492 | type Server struct { 493 | Name string `json:"name" xml:"name"` 494 | Port int `json:"port" xml:"port" bson:"port"` 495 | EnableLogs bool `json:"enable_logs" xml:"enable_logs" bson:"enable_logs"` 496 | BaseDomain string `json:"base_domain" xml:"base_domain" bson:"base_domain"` 497 | Credentials struct { 498 | Username string `json:"username" xml:"username"` 499 | Password string `json:"password" xml:"password"` 500 | } `json:"credentials" xml:"credentials"` 501 | } 502 | ``` 503 | 504 | ## Apply() 505 | 506 | In addition to the command line execution, gomodifytags is also available 507 | as a library `modifytags`, with a single method `Apply`. 508 | 509 | This method applies the struct tag modification of the receiver to all struct 510 | fields contained within a given node between given start and end positions. 511 | 512 | ## Editor integration 513 | 514 | Editors can use the tool by calling the tool and then either replace the buffer 515 | with the stdout or use the `-w` flag. 516 | 517 | Also `-line` and `-offset` flags should be preferred to be used with editors. 518 | An editor can select a range of lines and then pass it to `-line` flag. The 519 | editor also can pass the offset under the cursor if it's inside the struct to 520 | `-offset` 521 | 522 | Editors also can use the `-format` flag to output a json output with the 523 | changed lines. This is useful if you want to explicitly replace the buffer with 524 | the given lines. For the file below: 525 | 526 | 527 | ```go 528 | package main 529 | 530 | type Server struct { 531 | Name string 532 | Port int 533 | EnableLogs bool 534 | BaseDomain string 535 | Credentials struct { 536 | Username string 537 | Password string 538 | } 539 | } 540 | ``` 541 | 542 | If we add the `xml` tag and tell to output the format in json with the 543 | `-format` flag, the following will be printed: 544 | 545 | ``` 546 | $ gomodifytags -file demo.go -struct Server -add-tags xml -format json 547 | ``` 548 | ```json 549 | { 550 | "start": 3, 551 | "end": 12, 552 | "lines": [ 553 | "type Server struct {", 554 | "\tName string `xml:\"name\"`", 555 | "\tPort int `xml:\"port\"`", 556 | "\tEnableLogs bool `xml:\"enable_logs\"`", 557 | "\tBaseDomain string `xml:\"base_domain\"`", 558 | "\tCredentials struct {", 559 | "\t\tUsername string `xml:\"username\"`", 560 | "\t\tPassword string `xml:\"password\"`", 561 | "\t} `xml:\"credentials\"`", 562 | "}" 563 | ] 564 | } 565 | ``` 566 | 567 | The output is defined with the following Go struct: 568 | 569 | ```go 570 | type output struct { 571 | Start int `json:"start"` 572 | End int `json:"end"` 573 | Lines []string `json:"lines"` 574 | } 575 | ``` 576 | 577 | The `start` and `end` specifies the positions in the file the `lines` will 578 | apply. With this information, you can replace the editor buffer by iterating 579 | over the `lines` and set it for the given range. An example how it's done in 580 | vim-go in Vimscript is: 581 | 582 | ```viml 583 | let index = 0 584 | for line_number in range(start, end) 585 | call setline(line_number, lines[index]) 586 | let index += 1 587 | endfor 588 | ``` 589 | 590 | ### Unsaved files 591 | 592 | Editors can supply `gomodifytags` with the contents of unsaved buffers by using 593 | the `-modified` flag and writing an archive to stdin. Files in the archive 594 | will be preferred over those on disk. 595 | 596 | Each archive entry consists of: 597 | - the file name, followed by a newline 598 | - the (decimal) file size, followed by a newline 599 | - the contents of the file 600 | 601 | # Development 602 | 603 | At least Go `v1.11.x` is required. Older versions might work, but it's not 604 | recommended. First, checkout the repository: 605 | 606 | ``` 607 | git clone https://github.com/fatih/gomodifytags.git 608 | ``` 609 | 610 | Start developing the code. To build a binary, execute: 611 | 612 | ``` 613 | go build 614 | ``` 615 | 616 | This will create a `gomodifytags` binary in the current directory. To test the 617 | package, run the following: 618 | 619 | ``` 620 | go test -v 621 | ``` 622 | 623 | If everything works fine, feel free to open a pull request with your changes. 624 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fatih/gomodifytags 2 | 3 | require ( 4 | github.com/fatih/camelcase v1.0.0 5 | github.com/fatih/structtag v1.2.0 6 | golang.org/x/tools v0.23.0 7 | ) 8 | 9 | go 1.13 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= 2 | github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= 3 | github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= 4 | github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= 5 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 6 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 7 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 8 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 9 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 10 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 11 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 12 | golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= 13 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 14 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 15 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 16 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 17 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 18 | golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= 19 | golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 20 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 21 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 22 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 23 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 24 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 25 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 26 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 27 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 28 | golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 29 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 30 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 31 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 32 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 33 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 34 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 35 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 36 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 37 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 38 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 40 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 41 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 42 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 43 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 44 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 45 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 46 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 47 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 48 | golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= 49 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 50 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 51 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 52 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 53 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 54 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 55 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 56 | golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= 57 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 58 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 59 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 60 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 61 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 62 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 63 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 64 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 65 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 66 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 67 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 68 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 69 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 70 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 71 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 72 | golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= 73 | golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= 74 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 75 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/json" 7 | "errors" 8 | "flag" 9 | "fmt" 10 | "go/ast" 11 | "go/format" 12 | "go/parser" 13 | "go/printer" 14 | "go/token" 15 | "io" 16 | "io/ioutil" 17 | "os" 18 | "strconv" 19 | "strings" 20 | 21 | "github.com/fatih/gomodifytags/modifytags" 22 | "golang.org/x/tools/go/buildutil" 23 | ) 24 | 25 | // structType contains a structType node and it's name. It's a convenient 26 | // helper type, because *ast.StructType doesn't contain the name of the struct. 27 | // Also, we want to be able to handle structs selected by their variable name, 28 | // so we can't simply use a TypeSpec. 29 | type structType struct { 30 | name string 31 | node *ast.StructType 32 | } 33 | 34 | // output is used usually by editors 35 | type output struct { 36 | Start int `json:"start"` 37 | End int `json:"end"` 38 | Lines []string `json:"lines"` 39 | Errors []string `json:"errors,omitempty"` 40 | } 41 | 42 | // A config defines how the input is parsed and formatted. 43 | type config struct { 44 | file string 45 | output string 46 | quiet bool 47 | write bool 48 | modified io.Reader 49 | 50 | offset int 51 | structName string 52 | fieldName string 53 | line string 54 | start, end int 55 | all bool 56 | 57 | fset *token.FileSet 58 | } 59 | 60 | func main() { 61 | if err := realMain(); err != nil { 62 | fmt.Fprintln(os.Stderr, err.Error()) 63 | os.Exit(1) 64 | } 65 | } 66 | 67 | func realMain() error { 68 | cfg, mod, err := parseFlags(os.Args[1:]) 69 | if err != nil { 70 | if err == flag.ErrHelp { 71 | return nil 72 | } 73 | return err 74 | } 75 | 76 | return run(cfg, mod) 77 | } 78 | 79 | func parseFlags(args []string) (*config, *modifytags.Modification, error) { 80 | var ( 81 | // file flags 82 | flagFile = flag.String("file", "", "Filename to be parsed") 83 | flagWrite = flag.Bool("w", false, "Write results to (source) file") 84 | flagQuiet = flag.Bool("quiet", false, "Don't print result to stdout") 85 | 86 | flagOutput = flag.String("format", "source", "Output format."+ 87 | "By default it's the whole file. Options: [source, json]") 88 | flagModified = flag.Bool("modified", false, "read an archive of modified files from standard input") 89 | 90 | // processing modes 91 | flagOffset = flag.Int("offset", 0, 92 | "Byte offset of the cursor position inside a struct."+ 93 | "Can be anwhere from the comment until closing bracket") 94 | flagLine = flag.String("line", "", 95 | "Line number of the field or a range of line. i.e: 4 or 4,8") 96 | flagStruct = flag.String("struct", "", "Struct name to be processed") 97 | flagField = flag.String("field", "", "Field name to be processed") 98 | flagAll = flag.Bool("all", false, "Select all structs to be processed") 99 | 100 | // tag flags 101 | flagRemoveTags = flag.String("remove-tags", "", 102 | "Remove tags for the comma separated list of keys") 103 | flagClearTags = flag.Bool("clear-tags", false, 104 | "Clear all tags") 105 | flagAddTags = flag.String("add-tags", "", 106 | "Adds tags for the comma separated list of keys."+ 107 | "Keys can contain a static value, i,e: json:foo") 108 | flagOverride = flag.Bool("override", false, "Override current tags when adding tags") 109 | flagSkipUnexportedFields = flag.Bool("skip-unexported", false, "Skip unexported fields") 110 | flagTransform = flag.String("transform", "snakecase", 111 | "Transform adds a transform rule when adding tags."+ 112 | " Current options: [snakecase, camelcase, lispcase, pascalcase, titlecase, keep]") 113 | flagSort = flag.Bool("sort", false, 114 | "Sort sorts the tags in increasing order according to the key name") 115 | 116 | // formatting 117 | flagFormatting = flag.String("template", "", 118 | "Format the given tag's value. i.e: \"column:{field}\", \"field_name={field}\"") 119 | 120 | // option flags 121 | flagRemoveOptions = flag.String("remove-options", "", 122 | "Remove the comma separated list of options from the given keys, "+ 123 | "i.e: json=omitempty,hcl=squash") 124 | flagClearOptions = flag.Bool("clear-options", false, 125 | "Clear all tag options") 126 | flagAddOptions = flag.String("add-options", "", 127 | "Add the options per given key. i.e: json=omitempty,hcl=squash") 128 | ) 129 | 130 | // this fails if there are flags re-defined with the same name. 131 | if err := flag.CommandLine.Parse(args); err != nil { 132 | return nil, nil, err 133 | } 134 | 135 | if flag.NFlag() == 0 { 136 | fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) 137 | flag.PrintDefaults() 138 | return nil, nil, flag.ErrHelp 139 | } 140 | 141 | cfg := &config{ 142 | file: *flagFile, 143 | line: *flagLine, 144 | structName: *flagStruct, 145 | fieldName: *flagField, 146 | offset: *flagOffset, 147 | all: *flagAll, 148 | output: *flagOutput, 149 | write: *flagWrite, 150 | quiet: *flagQuiet, 151 | } 152 | 153 | transform, err := parseTransform(*flagTransform) 154 | if err != nil { 155 | return nil, nil, err 156 | } 157 | 158 | mod := &modifytags.Modification{ 159 | Clear: *flagClearTags, 160 | ClearOptions: *flagClearOptions, 161 | Transform: transform, 162 | Sort: *flagSort, 163 | ValueFormat: *flagFormatting, 164 | Overwrite: *flagOverride, 165 | SkipUnexportedFields: *flagSkipUnexportedFields, 166 | } 167 | 168 | if *flagModified { 169 | cfg.modified = os.Stdin 170 | } 171 | 172 | if *flagAddTags != "" { 173 | mod.Add = strings.Split(*flagAddTags, ",") 174 | } 175 | 176 | if *flagAddOptions != "" { 177 | parsedOptions, err := parseOptions(*flagAddOptions) 178 | if err != nil { 179 | return nil, nil, err 180 | } 181 | mod.AddOptions = parsedOptions 182 | } 183 | 184 | if *flagRemoveTags != "" { 185 | mod.Remove = strings.Split(*flagRemoveTags, ",") 186 | } 187 | 188 | if *flagRemoveOptions != "" { 189 | parsedOptions, err := parseOptions(*flagRemoveOptions) 190 | if err != nil { 191 | return nil, nil, err 192 | } 193 | mod.RemoveOptions = parsedOptions 194 | } 195 | 196 | return cfg, mod, nil 197 | } 198 | 199 | func run(cfg *config, mod *modifytags.Modification) error { 200 | err := cfg.validate() 201 | if err != nil { 202 | return err 203 | } 204 | 205 | file, err := cfg.parse() 206 | if err != nil { 207 | return err 208 | } 209 | 210 | start, end, err := cfg.findSelection(file) 211 | if err != nil { 212 | return err 213 | } 214 | cfg.start = cfg.fset.Position(start).Line 215 | cfg.end = cfg.fset.Position(end).Line 216 | 217 | errs := mod.Apply(cfg.fset, file, start, end) 218 | if errs != nil { 219 | if _, ok := errs.(*modifytags.RewriteErrors); !ok { 220 | return errs 221 | } 222 | } 223 | var out string 224 | if errs != nil { 225 | out, err = cfg.format(file, errs.(*modifytags.RewriteErrors)) 226 | } else { 227 | out, err = cfg.format(file, nil) 228 | } 229 | 230 | if err != nil { 231 | return err 232 | } 233 | 234 | if !cfg.quiet { 235 | fmt.Println(out) 236 | } 237 | return nil 238 | } 239 | 240 | func (cfg *config) parse() (*ast.File, error) { 241 | cfg.fset = token.NewFileSet() 242 | var contents interface{} 243 | if cfg.modified != nil { 244 | archive, err := buildutil.ParseOverlayArchive(cfg.modified) 245 | if err != nil { 246 | return nil, fmt.Errorf("failed to parse -modified archive: %v", err) 247 | } 248 | fc, ok := archive[cfg.file] 249 | if !ok { 250 | return nil, fmt.Errorf("couldn't find %s in archive", cfg.file) 251 | } 252 | contents = fc 253 | } 254 | 255 | return parser.ParseFile(cfg.fset, cfg.file, contents, parser.ParseComments) 256 | } 257 | 258 | func parseTransform(input string) (modifytags.Transform, error) { 259 | input = strings.ToLower(input) 260 | switch input { 261 | case "camelcase": 262 | return modifytags.CamelCase, nil 263 | case "lispcase": 264 | return modifytags.LispCase, nil 265 | case "pascalcase": 266 | return modifytags.PascalCase, nil 267 | case "titlecase": 268 | return modifytags.TitleCase, nil 269 | case "keep": 270 | return modifytags.Keep, nil 271 | case "snakecase": 272 | return modifytags.SnakeCase, nil 273 | default: 274 | return modifytags.SnakeCase, fmt.Errorf("invalid transform value") 275 | } 276 | } 277 | 278 | func parseOptions(options string) (map[string][]string, error) { 279 | optionsMap := make(map[string][]string) 280 | list := strings.Split(options, ",") 281 | for _, item := range list { 282 | key, option, found := strings.Cut(item, "=") 283 | if !found { 284 | return nil, fmt.Errorf("invalid option %q; should be key=option", item) 285 | } 286 | optionsMap[key] = append(optionsMap[key], option) 287 | } 288 | return optionsMap, nil 289 | } 290 | 291 | // findSelection returns the start and end positions of the fields that are 292 | // subject to change. It depends on the line, struct or offset selection. 293 | func (cfg *config) findSelection(file *ast.File) (token.Pos, token.Pos, error) { 294 | switch { 295 | case cfg.line != "": 296 | return cfg.lineSelection(file) 297 | case cfg.offset != 0: 298 | return cfg.offsetSelection(file) 299 | case cfg.structName != "": 300 | return cfg.structSelection(file) 301 | case cfg.all: 302 | return cfg.allSelection(file) 303 | default: 304 | return 0, 0, errors.New("-line, -offset, -struct or -all is not passed") 305 | } 306 | } 307 | 308 | // collectStructs collects and maps structType nodes to their positions 309 | func collectStructs(node ast.Node) map[token.Pos]*structType { 310 | structs := make(map[token.Pos]*structType, 0) 311 | 312 | collectStructs := func(n ast.Node) bool { 313 | var t ast.Expr 314 | var structName string 315 | 316 | switch x := n.(type) { 317 | case *ast.TypeSpec: 318 | if x.Type == nil { 319 | return true 320 | 321 | } 322 | 323 | structName = x.Name.Name 324 | t = x.Type 325 | case *ast.CompositeLit: 326 | t = x.Type 327 | case *ast.ValueSpec: 328 | structName = x.Names[0].Name 329 | t = x.Type 330 | case *ast.Field: 331 | // this case also catches struct fields and the structName 332 | // therefore might contain the field name (which is wrong) 333 | // because `x.Type` in this case is not a *ast.StructType. 334 | // 335 | // We're OK with it, because, in our case *ast.Field represents 336 | // a parameter declaration, i.e: 337 | // 338 | // func test(arg struct { 339 | // Field int 340 | // }) { 341 | // } 342 | // 343 | // and hence the struct name will be `arg`. 344 | if len(x.Names) != 0 { 345 | structName = x.Names[0].Name 346 | } 347 | t = x.Type 348 | } 349 | 350 | // if expression is in form "*T" or "[]T", dereference to check if "T" 351 | // contains a struct expression 352 | t = deref(t) 353 | 354 | x, ok := t.(*ast.StructType) 355 | if !ok { 356 | return true 357 | } 358 | 359 | structs[x.Pos()] = &structType{ 360 | name: structName, 361 | node: x, 362 | } 363 | return true 364 | } 365 | 366 | ast.Inspect(node, collectStructs) 367 | return structs 368 | } 369 | 370 | func (cfg *config) format(file ast.Node, rwErr *modifytags.RewriteErrors) (string, error) { 371 | switch cfg.output { 372 | case "source": 373 | var buf bytes.Buffer 374 | err := format.Node(&buf, cfg.fset, file) 375 | if err != nil { 376 | return "", err 377 | } 378 | 379 | if cfg.write { 380 | err = ioutil.WriteFile(cfg.file, buf.Bytes(), 0) 381 | if err != nil { 382 | return "", err 383 | } 384 | 385 | } 386 | 387 | return buf.String(), nil 388 | case "json": 389 | // NOTE(arslan): print first the whole file and then cut out our 390 | // selection. The reason we don't directly print the struct is that the 391 | // printer is not capable of printing loosy comments, comments that are 392 | // not part of any field inside a struct. Those are part of *ast.File 393 | // and only printed inside a struct if we print the whole file. This 394 | // approach is the sanest and simplest way to get a struct printed 395 | // back. Second, our cursor might intersect two different structs with 396 | // other declarations in between them. Printing the file and cutting 397 | // the selection is the easier and simpler to do. 398 | var buf bytes.Buffer 399 | 400 | // this is the default config from `format.Node()`, but we add 401 | // `printer.SourcePos` to get the original source position of the 402 | // modified lines 403 | pcfg := printer.Config{Mode: printer.SourcePos | printer.UseSpaces | printer.TabIndent, Tabwidth: 8} 404 | err := pcfg.Fprint(&buf, cfg.fset, file) 405 | if err != nil { 406 | return "", err 407 | } 408 | 409 | lines, err := parseLines(&buf) 410 | if err != nil { 411 | return "", err 412 | } 413 | 414 | // prevent selection to be larger than the actual number of lines 415 | if cfg.start > len(lines) || cfg.end > len(lines) { 416 | return "", errors.New("line selection is invalid") 417 | } 418 | 419 | out := &output{ 420 | Start: cfg.start, 421 | End: cfg.end, 422 | Lines: lines[cfg.start-1 : cfg.end], 423 | } 424 | 425 | if rwErr != nil { 426 | for _, err := range rwErr.Errs { 427 | out.Errors = append(out.Errors, err.Error()) 428 | } 429 | } 430 | 431 | o, err := json.MarshalIndent(out, "", " ") 432 | if err != nil { 433 | return "", err 434 | } 435 | 436 | return string(o), nil 437 | default: 438 | return "", fmt.Errorf("unknown output mode: %s", cfg.output) 439 | } 440 | } 441 | 442 | func (cfg *config) lineSelection(file *ast.File) (token.Pos, token.Pos, error) { 443 | var err error 444 | split := strings.Split(cfg.line, ",") 445 | 446 | start, err := strconv.Atoi(split[0]) 447 | if err != nil { 448 | return 0, 0, err 449 | } 450 | 451 | end := start 452 | if len(split) == 2 { 453 | end, err = strconv.Atoi(split[1]) 454 | if err != nil { 455 | return 0, 0, err 456 | } 457 | } 458 | 459 | if start > end { 460 | return 0, 0, errors.New("wrong range. start line cannot be larger than end line") 461 | } 462 | 463 | // Convert start and end line numbers to token.Pos. 464 | tokFile := cfg.fset.File(file.FileStart) 465 | lineCount := tokFile.LineCount() 466 | if start < 0 || start > lineCount || end > lineCount { 467 | return 0, 0, fmt.Errorf("line selection %q is invalid: %d is not within [0, %d]", cfg.line, start, lineCount) 468 | } 469 | startPos := tokFile.LineStart(start) 470 | var endPos token.Pos 471 | if end == tokFile.LineCount() { 472 | endPos = tokFile.Pos(tokFile.Size()) 473 | } else { 474 | // Get the position of the end of the line 475 | endPos = tokFile.LineStart(end+1) - 1 476 | } 477 | return startPos, endPos, nil 478 | } 479 | 480 | func (cfg *config) structSelection(file *ast.File) (token.Pos, token.Pos, error) { 481 | structs := collectStructs(file) 482 | 483 | var encStruct *ast.StructType 484 | for _, st := range structs { 485 | if st.name == cfg.structName { 486 | encStruct = st.node 487 | } 488 | } 489 | 490 | if encStruct == nil { 491 | return 0, 0, errors.New("struct name does not exist") 492 | } 493 | 494 | // if field name has been specified as well, only select the given field 495 | if cfg.fieldName != "" { 496 | return cfg.fieldSelection(encStruct) 497 | } 498 | return encStruct.Pos(), encStruct.End(), nil 499 | } 500 | 501 | func (cfg *config) fieldSelection(st *ast.StructType) (token.Pos, token.Pos, error) { 502 | var encField *ast.Field 503 | for _, f := range st.Fields.List { 504 | for _, field := range f.Names { 505 | if field.Name == cfg.fieldName { 506 | encField = f 507 | } 508 | } 509 | } 510 | 511 | if encField == nil { 512 | return 0, 0, fmt.Errorf("struct %q doesn't have field name %q", 513 | cfg.structName, cfg.fieldName) 514 | } 515 | return encField.Pos(), encField.End(), nil 516 | } 517 | 518 | func (cfg *config) offsetSelection(file *ast.File) (token.Pos, token.Pos, error) { 519 | structs := collectStructs(file) 520 | 521 | var encStruct *ast.StructType 522 | for _, st := range structs { 523 | structBegin := cfg.fset.Position(st.node.Pos()).Offset 524 | structEnd := cfg.fset.Position(st.node.End()).Offset 525 | 526 | if structBegin <= cfg.offset && cfg.offset <= structEnd { 527 | encStruct = st.node 528 | break 529 | } 530 | } 531 | 532 | if encStruct == nil { 533 | return 0, 0, errors.New("offset is not inside a struct") 534 | } 535 | 536 | // offset selects all fields 537 | return encStruct.Pos(), encStruct.End(), nil 538 | } 539 | 540 | // allSelection selects all structs inside a file 541 | func (cfg *config) allSelection(file *ast.File) (token.Pos, token.Pos, error) { 542 | tokFile := cfg.fset.File(file.FileStart) 543 | return tokFile.LineStart(1), tokFile.Pos(tokFile.Size()), nil 544 | } 545 | 546 | // validate determines whether the config is valid or not 547 | func (cfg *config) validate() error { 548 | if cfg.file == "" { 549 | return errors.New("no file is passed") 550 | } 551 | 552 | if cfg.line == "" && cfg.offset == 0 && cfg.structName == "" && !cfg.all { 553 | return errors.New("-line, -offset, -struct or -all is not passed") 554 | } 555 | 556 | if cfg.line != "" && cfg.offset != 0 || 557 | cfg.line != "" && cfg.structName != "" || 558 | cfg.offset != 0 && cfg.structName != "" { 559 | return errors.New("-line, -offset or -struct cannot be used together. pick one") 560 | } 561 | 562 | if cfg.fieldName != "" && cfg.structName == "" { 563 | return errors.New("-field is requiring -struct") 564 | } 565 | 566 | return nil 567 | } 568 | 569 | // parseLines parses the given buffer and returns a slice of lines 570 | func parseLines(buf io.Reader) ([]string, error) { 571 | var lines []string 572 | scanner := bufio.NewScanner(buf) 573 | for scanner.Scan() { 574 | txt := scanner.Text() 575 | 576 | // check for any line directive and store it for next iteration to 577 | // re-construct the original file. If it's not a line directive, 578 | // continue consturcting the original file 579 | if !strings.HasPrefix(txt, "//line") { 580 | lines = append(lines, txt) 581 | continue 582 | } 583 | 584 | lineNr, err := split(txt) 585 | if err != nil { 586 | return nil, err 587 | } 588 | 589 | for i := len(lines); i < lineNr-1; i++ { 590 | lines = append(lines, "") 591 | } 592 | 593 | lines = lines[:lineNr-1] 594 | } 595 | 596 | if err := scanner.Err(); err != nil { 597 | return nil, fmt.Errorf("invalid scanner inputl: %s", err) 598 | } 599 | 600 | return lines, nil 601 | } 602 | 603 | // split splits the given line directive and returns the line number 604 | // see https://golang.org/cmd/compile/#hdr-Compiler_Directives for more 605 | // information 606 | // NOTE(arslan): this only splits the line directive that the go.Parser 607 | // outputs. If the go parser changes the format of the line directive, make 608 | // sure to fix it in the below function 609 | func split(line string) (int, error) { 610 | for i := len(line) - 1; i >= 0; i-- { 611 | if line[i] != ':' { 612 | continue 613 | } 614 | 615 | nr, err := strconv.Atoi(line[i+1:]) 616 | if err != nil { 617 | return 0, err 618 | } 619 | 620 | return nr, nil 621 | } 622 | 623 | return 0, fmt.Errorf("couldn't parse line: '%s'", line) 624 | } 625 | 626 | // deref takes an expression, and removes all its leading "*" and "[]" 627 | // operator. Uuse case : if found expression is a "*t" or "[]t", we need to 628 | // check if "t" contains a struct expression. 629 | func deref(x ast.Expr) ast.Expr { 630 | switch t := x.(type) { 631 | case *ast.StarExpr: 632 | return deref(t.X) 633 | case *ast.ArrayType: 634 | return deref(t.Elt) 635 | } 636 | return x 637 | } 638 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | "path/filepath" 11 | "reflect" 12 | "strings" 13 | "testing" 14 | 15 | "github.com/fatih/gomodifytags/modifytags" 16 | ) 17 | 18 | var update = flag.Bool("update", false, "update golden (.out) files") 19 | 20 | // This is the directory where our test fixtures are. 21 | const fixtureDir = "./test-fixtures" 22 | 23 | func TestParseFlags(t *testing.T) { 24 | // don't output help message during the test 25 | flag.CommandLine.SetOutput(ioutil.Discard) 26 | 27 | // The flag.CommandLine.Parse() call fails if there are flags re-defined 28 | // with the same name. If there are duplicates, parseFlags() will return 29 | // an error. 30 | _, _, err := parseFlags([]string{"-struct", "Server", "-add-tags", "json,xml", "-transform", "invalid"}) 31 | if err == nil || !reflect.DeepEqual("invalid transform value", err.Error()) { 32 | t.Fatal("expected error: " + err.Error()) 33 | } 34 | } 35 | 36 | func TestRewrite(t *testing.T) { 37 | test := []struct { 38 | cfg *config 39 | mod *modifytags.Modification 40 | file string 41 | err error 42 | }{ 43 | { 44 | file: "struct_add", 45 | cfg: &config{ 46 | output: "source", 47 | structName: "foo", 48 | }, 49 | mod: &modifytags.Modification{ 50 | Add: []string{"json"}, 51 | Transform: modifytags.SnakeCase, 52 | }, 53 | }, 54 | { 55 | file: "struct_add_underscore", 56 | cfg: &config{ 57 | output: "source", 58 | structName: "foo", 59 | }, 60 | mod: &modifytags.Modification{ 61 | Add: []string{"json"}, 62 | Transform: modifytags.SnakeCase, 63 | }, 64 | }, 65 | { 66 | file: "struct_add_existing", 67 | 68 | cfg: &config{ 69 | output: "source", 70 | structName: "foo", 71 | }, 72 | mod: &modifytags.Modification{ 73 | Add: []string{"json"}, 74 | Transform: modifytags.SnakeCase, 75 | }, 76 | }, 77 | { 78 | file: "struct_format", 79 | cfg: &config{ 80 | output: "source", 81 | structName: "foo", 82 | }, 83 | mod: &modifytags.Modification{ 84 | Add: []string{"gaum"}, 85 | Transform: modifytags.SnakeCase, 86 | ValueFormat: "field_name={field}", 87 | }, 88 | }, 89 | { 90 | file: "struct_format_existing", 91 | cfg: &config{ 92 | output: "source", 93 | structName: "foo", 94 | }, 95 | mod: &modifytags.Modification{ 96 | Add: []string{"gaum"}, 97 | Transform: modifytags.SnakeCase, 98 | ValueFormat: "field_name={field}", 99 | }, 100 | }, 101 | { 102 | file: "struct_format_oldstyle", 103 | cfg: &config{ 104 | output: "source", 105 | structName: "foo", 106 | }, 107 | mod: &modifytags.Modification{ 108 | Add: []string{"gaum"}, 109 | Transform: modifytags.SnakeCase, 110 | ValueFormat: "field_name={field}", 111 | }, 112 | }, 113 | { 114 | file: "struct_format_existing_oldstyle", 115 | cfg: &config{ 116 | output: "source", 117 | structName: "foo", 118 | }, 119 | mod: &modifytags.Modification{ 120 | Add: []string{"gaum"}, 121 | Transform: modifytags.SnakeCase, 122 | ValueFormat: "field_name={field}", 123 | }, 124 | }, 125 | { 126 | file: "struct_remove", 127 | cfg: &config{ 128 | output: "source", 129 | structName: "foo", 130 | }, 131 | mod: &modifytags.Modification{ 132 | Remove: []string{"json"}, 133 | }, 134 | }, 135 | { 136 | file: "struct_clear_tags", 137 | cfg: &config{ 138 | output: "source", 139 | structName: "foo", 140 | }, 141 | mod: &modifytags.Modification{ 142 | Clear: true, 143 | }, 144 | }, 145 | { 146 | file: "struct_clear_options", 147 | cfg: &config{ 148 | output: "source", 149 | structName: "foo", 150 | }, 151 | mod: &modifytags.Modification{ 152 | ClearOptions: true, 153 | }, 154 | }, 155 | { 156 | file: "line_add", 157 | cfg: &config{ 158 | output: "source", 159 | line: "4", 160 | }, 161 | mod: &modifytags.Modification{ 162 | Add: []string{"json"}, 163 | Transform: modifytags.SnakeCase, 164 | }, 165 | }, 166 | { 167 | file: "line_add_override", 168 | cfg: &config{ 169 | output: "source", 170 | line: "4,5", 171 | }, 172 | mod: &modifytags.Modification{ 173 | Add: []string{"json"}, 174 | Transform: modifytags.SnakeCase, 175 | Overwrite: true, 176 | }, 177 | }, 178 | { 179 | file: "line_add_override_column", 180 | cfg: &config{ 181 | output: "source", 182 | line: "4,4", 183 | }, 184 | mod: &modifytags.Modification{ 185 | Add: []string{"json:MyBar:bar"}, 186 | Transform: modifytags.SnakeCase, 187 | Overwrite: true, 188 | }, 189 | }, 190 | { 191 | file: "line_add_override_mixed_column_and_equal", 192 | cfg: &config{ 193 | output: "source", 194 | line: "4,4", 195 | }, 196 | mod: &modifytags.Modification{ 197 | Add: []string{"json:MyBar:bar:foo=qux"}, 198 | Transform: modifytags.SnakeCase, 199 | Overwrite: true, 200 | }, 201 | }, 202 | { 203 | file: "line_add_override_multi_equal", 204 | cfg: &config{ 205 | output: "source", 206 | line: "4,4", 207 | }, 208 | mod: &modifytags.Modification{ 209 | Add: []string{"json:MyBar=bar=foo"}, 210 | Transform: modifytags.SnakeCase, 211 | Overwrite: true, 212 | }, 213 | }, 214 | { 215 | file: "line_add_override_multi_column", 216 | cfg: &config{ 217 | output: "source", 218 | line: "4,4", 219 | }, 220 | mod: &modifytags.Modification{ 221 | Add: []string{"json:MyBar:bar:foo"}, 222 | Transform: modifytags.SnakeCase, 223 | Overwrite: true, 224 | }, 225 | }, 226 | { 227 | file: "line_add_no_override", 228 | cfg: &config{ 229 | output: "source", 230 | line: "4,5", 231 | }, 232 | mod: &modifytags.Modification{ 233 | Add: []string{"json"}, 234 | Transform: modifytags.SnakeCase, 235 | }, 236 | }, 237 | { 238 | file: "line_add_outside", 239 | cfg: &config{ 240 | output: "source", 241 | line: "2,8", 242 | }, 243 | mod: &modifytags.Modification{ 244 | Add: []string{"json"}, 245 | Transform: modifytags.SnakeCase, 246 | }, 247 | }, 248 | { 249 | file: "line_add_outside_partial_start", 250 | cfg: &config{ 251 | output: "source", 252 | line: "2,5", 253 | }, 254 | mod: &modifytags.Modification{ 255 | Add: []string{"json"}, 256 | Transform: modifytags.SnakeCase, 257 | }, 258 | }, 259 | { 260 | file: "line_add_outside_partial_end", 261 | cfg: &config{ 262 | output: "source", 263 | line: "5,8", 264 | }, 265 | mod: &modifytags.Modification{ 266 | Add: []string{"json"}, 267 | Transform: modifytags.SnakeCase, 268 | }, 269 | }, 270 | { 271 | file: "line_add_intersect_partial", 272 | cfg: &config{ 273 | output: "source", 274 | line: "5,11", 275 | }, 276 | mod: &modifytags.Modification{ 277 | Add: []string{"json"}, 278 | Transform: modifytags.SnakeCase, 279 | }, 280 | }, 281 | { 282 | file: "line_add_comment", 283 | cfg: &config{ 284 | output: "source", 285 | line: "6,7", 286 | }, 287 | mod: &modifytags.Modification{ 288 | Add: []string{"json"}, 289 | Transform: modifytags.SnakeCase, 290 | }, 291 | }, 292 | { 293 | file: "line_add_option", 294 | cfg: &config{ 295 | output: "source", 296 | line: "4,7", 297 | }, 298 | mod: &modifytags.Modification{ 299 | AddOptions: map[string][]string{ 300 | "json": {"omitempty"}, 301 | }, 302 | }, 303 | }, 304 | { 305 | file: "line_add_option_existing", 306 | cfg: &config{ 307 | output: "source", 308 | line: "6,8", 309 | }, 310 | mod: &modifytags.Modification{ 311 | AddOptions: map[string][]string{ 312 | "json": {"omitempty"}, 313 | }, 314 | }, 315 | }, 316 | { 317 | file: "line_add_multiple_option", 318 | cfg: &config{ 319 | output: "source", 320 | line: "4,7", 321 | }, 322 | mod: &modifytags.Modification{ 323 | AddOptions: map[string][]string{ 324 | "json": {"omitempty"}, 325 | "hcl": {"squash"}, 326 | }, 327 | Add: []string{"hcl"}, 328 | Transform: modifytags.SnakeCase, 329 | }, 330 | }, 331 | { 332 | file: "line_add_option_with_equal", 333 | cfg: &config{ 334 | output: "source", 335 | line: "4,7", 336 | }, 337 | mod: &modifytags.Modification{ 338 | AddOptions: map[string][]string{ 339 | "validate": {"max=32"}, 340 | }, 341 | Add: []string{"validate"}, 342 | Transform: modifytags.SnakeCase, 343 | }, 344 | }, 345 | { 346 | file: "line_remove", 347 | cfg: &config{ 348 | output: "source", 349 | line: "5,7", 350 | }, 351 | mod: &modifytags.Modification{ 352 | Remove: []string{"json"}, 353 | }, 354 | }, 355 | { 356 | file: "line_remove_option", 357 | cfg: &config{ 358 | output: "source", 359 | line: "4,8", 360 | }, 361 | mod: &modifytags.Modification{ 362 | RemoveOptions: map[string][]string{ 363 | "hcl": {"squash"}, 364 | }, 365 | }, 366 | }, 367 | { 368 | file: "line_remove_options", 369 | cfg: &config{ 370 | output: "source", 371 | line: "4,7", 372 | }, 373 | mod: &modifytags.Modification{ 374 | RemoveOptions: map[string][]string{ 375 | "hcl": {"omitnested"}, 376 | "json": {"omitempty"}, 377 | }, 378 | }, 379 | }, 380 | { 381 | file: "line_remove_option_with_equal", 382 | cfg: &config{ 383 | output: "source", 384 | line: "4,7", 385 | }, 386 | mod: &modifytags.Modification{ 387 | RemoveOptions: map[string][]string{ 388 | "validate": {"max=32"}, 389 | }, 390 | }, 391 | }, 392 | { 393 | file: "line_multiple_add", 394 | cfg: &config{ 395 | output: "source", 396 | line: "5,6", 397 | }, 398 | mod: &modifytags.Modification{ 399 | Add: []string{"json"}, 400 | Transform: modifytags.CamelCase, 401 | }, 402 | }, 403 | { 404 | file: "line_lispcase_add", 405 | cfg: &config{ 406 | output: "source", 407 | line: "4,6", 408 | }, 409 | mod: &modifytags.Modification{ 410 | Add: []string{"json"}, 411 | Transform: modifytags.LispCase, 412 | }, 413 | }, 414 | { 415 | file: "line_camelcase_add", 416 | cfg: &config{ 417 | output: "source", 418 | line: "4,5", 419 | }, 420 | mod: &modifytags.Modification{ 421 | Add: []string{"json"}, 422 | Transform: modifytags.CamelCase, 423 | }, 424 | }, 425 | { 426 | file: "line_camelcase_add_embedded", 427 | cfg: &config{ 428 | output: "source", 429 | line: "4,6", 430 | }, 431 | mod: &modifytags.Modification{ 432 | Add: []string{"json"}, 433 | Transform: modifytags.CamelCase, 434 | }, 435 | }, 436 | { 437 | file: "line_value_add", 438 | cfg: &config{ 439 | output: "source", 440 | line: "4,6", 441 | }, 442 | mod: &modifytags.Modification{ 443 | Add: []string{"json:foo"}, 444 | }, 445 | }, 446 | { 447 | file: "offset_add", 448 | cfg: &config{ 449 | output: "source", 450 | offset: 32, 451 | }, 452 | mod: &modifytags.Modification{ 453 | Add: []string{"json"}, 454 | Transform: modifytags.SnakeCase, 455 | }, 456 | }, 457 | { 458 | file: "offset_add_composite", 459 | cfg: &config{ 460 | output: "source", 461 | offset: 40, 462 | }, 463 | mod: &modifytags.Modification{ 464 | Add: []string{"json"}, 465 | Transform: modifytags.SnakeCase, 466 | }, 467 | }, 468 | { 469 | file: "offset_add_duplicate", 470 | cfg: &config{ 471 | output: "source", 472 | offset: 209, 473 | }, 474 | mod: &modifytags.Modification{ 475 | Add: []string{"json"}, 476 | Transform: modifytags.SnakeCase, 477 | }, 478 | }, 479 | { 480 | file: "offset_add_literal_in", 481 | cfg: &config{ 482 | output: "source", 483 | offset: 46, 484 | }, 485 | mod: &modifytags.Modification{ 486 | Add: []string{"json"}, 487 | Transform: modifytags.SnakeCase, 488 | }, 489 | }, 490 | { 491 | file: "offset_add_literal_out", 492 | cfg: &config{ 493 | output: "source", 494 | offset: 32, 495 | }, 496 | mod: &modifytags.Modification{ 497 | Add: []string{"json"}, 498 | Transform: modifytags.SnakeCase, 499 | }, 500 | }, 501 | { 502 | file: "errors", 503 | cfg: &config{ 504 | output: "source", 505 | line: "4,7", 506 | }, 507 | mod: &modifytags.Modification{ 508 | Add: []string{"json"}, 509 | Transform: modifytags.SnakeCase, 510 | }, 511 | }, 512 | { 513 | file: "line_pascalcase_add", 514 | cfg: &config{ 515 | output: "source", 516 | line: "4,5", 517 | }, 518 | mod: &modifytags.Modification{ 519 | Add: []string{"json"}, 520 | Transform: modifytags.PascalCase, 521 | }, 522 | }, 523 | { 524 | file: "line_pascalcase_add_embedded", 525 | cfg: &config{ 526 | output: "source", 527 | line: "4,6", 528 | }, 529 | mod: &modifytags.Modification{ 530 | Add: []string{"json"}, 531 | Transform: modifytags.PascalCase, 532 | }, 533 | }, 534 | { 535 | file: "not_formatted", 536 | cfg: &config{ 537 | output: "source", 538 | line: "3,4", 539 | }, 540 | mod: &modifytags.Modification{ 541 | Add: []string{"json"}, 542 | Transform: modifytags.SnakeCase, 543 | }, 544 | }, 545 | { 546 | file: "skip_private", 547 | cfg: &config{ 548 | output: "source", 549 | structName: "foo", 550 | }, 551 | mod: &modifytags.Modification{ 552 | Add: []string{"json"}, 553 | Transform: modifytags.SnakeCase, 554 | SkipUnexportedFields: true, 555 | }, 556 | }, 557 | { 558 | file: "skip_private_multiple_names", 559 | cfg: &config{ 560 | output: "source", 561 | structName: "foo", 562 | }, 563 | mod: &modifytags.Modification{ 564 | Add: []string{"json"}, 565 | Transform: modifytags.SnakeCase, 566 | SkipUnexportedFields: true, 567 | }, 568 | }, 569 | { 570 | file: "skip_embedded", 571 | cfg: &config{ 572 | output: "source", 573 | structName: "StationCreated", 574 | }, 575 | mod: &modifytags.Modification{ 576 | Add: []string{"json"}, 577 | Transform: modifytags.SnakeCase, 578 | SkipUnexportedFields: true, 579 | }, 580 | }, 581 | { 582 | file: "all_structs", 583 | cfg: &config{ 584 | output: "source", 585 | all: true, 586 | }, 587 | mod: &modifytags.Modification{ 588 | Add: []string{"json"}, 589 | Transform: modifytags.SnakeCase, 590 | }, 591 | }, 592 | { 593 | file: "line_titlecase_add", 594 | cfg: &config{ 595 | output: "source", 596 | line: "4,6", 597 | }, 598 | mod: &modifytags.Modification{ 599 | Add: []string{"json"}, 600 | Transform: modifytags.TitleCase, 601 | }, 602 | }, 603 | { 604 | file: "line_titlecase_add_embedded", 605 | cfg: &config{ 606 | output: "source", 607 | line: "4,6", 608 | }, 609 | mod: &modifytags.Modification{ 610 | Add: []string{"json"}, 611 | Transform: modifytags.TitleCase, 612 | }, 613 | }, 614 | { 615 | file: "field_add", 616 | cfg: &config{ 617 | output: "source", 618 | structName: "foo", 619 | fieldName: "bar", 620 | }, 621 | mod: &modifytags.Modification{ 622 | Add: []string{"json"}, 623 | Transform: modifytags.SnakeCase, 624 | }, 625 | }, 626 | { 627 | file: "field_add_same_line", 628 | cfg: &config{ 629 | output: "source", 630 | structName: "foo", 631 | fieldName: "qux", 632 | }, 633 | mod: &modifytags.Modification{ 634 | Add: []string{"json"}, 635 | Transform: modifytags.SnakeCase, 636 | }, 637 | }, 638 | { 639 | file: "field_add_existing", 640 | cfg: &config{ 641 | output: "source", 642 | structName: "foo", 643 | fieldName: "bar", 644 | }, 645 | mod: &modifytags.Modification{ 646 | Add: []string{"json"}, 647 | Transform: modifytags.SnakeCase, 648 | }, 649 | }, 650 | { 651 | file: "field_clear_tags", 652 | cfg: &config{ 653 | output: "source", 654 | structName: "foo", 655 | fieldName: "bar", 656 | }, 657 | mod: &modifytags.Modification{ 658 | Clear: true, 659 | }, 660 | }, 661 | { 662 | file: "field_clear_options", 663 | cfg: &config{ 664 | output: "source", 665 | structName: "foo", 666 | fieldName: "bar", 667 | }, 668 | mod: &modifytags.Modification{ 669 | ClearOptions: true, 670 | }, 671 | }, 672 | { 673 | file: "field_remove", 674 | cfg: &config{ 675 | output: "source", 676 | structName: "foo", 677 | fieldName: "bar", 678 | }, 679 | mod: &modifytags.Modification{ 680 | Remove: []string{"json"}, 681 | }, 682 | }, 683 | { 684 | file: "offset_anonymous_struct", 685 | cfg: &config{ 686 | output: "source", 687 | offset: 45, 688 | }, 689 | mod: &modifytags.Modification{ 690 | Add: []string{"json"}, 691 | Transform: modifytags.CamelCase, 692 | }, 693 | }, 694 | { 695 | file: "offset_star_struct", 696 | cfg: &config{ 697 | output: "source", 698 | offset: 35, 699 | }, 700 | mod: &modifytags.Modification{ 701 | Add: []string{"json"}, 702 | Transform: modifytags.CamelCase, 703 | }, 704 | }, 705 | { 706 | file: "offset_array_struct", 707 | cfg: &config{ 708 | output: "source", 709 | offset: 35, 710 | }, 711 | mod: &modifytags.Modification{ 712 | Add: []string{"json"}, 713 | Transform: modifytags.CamelCase, 714 | }, 715 | }, 716 | { 717 | file: "empty_file", 718 | cfg: &config{ 719 | output: "source", 720 | all: true, 721 | }, 722 | mod: &modifytags.Modification{ 723 | Add: []string{"json"}, 724 | }, 725 | }, 726 | { 727 | file: "empty_file", 728 | cfg: &config{ 729 | output: "source", 730 | line: "4,6", 731 | }, 732 | mod: &modifytags.Modification{ 733 | Add: []string{"json"}, 734 | }, 735 | err: errors.New("line selection \"4,6\" is invalid: 4 is not within [0, 1]"), 736 | }, 737 | } 738 | 739 | for _, ts := range test { 740 | t.Run(ts.file, func(t *testing.T) { 741 | ts.cfg.file = filepath.Join(fixtureDir, fmt.Sprintf("%s.input", ts.file)) 742 | 743 | node, err := ts.cfg.parse() 744 | if err != nil { 745 | t.Fatal(err) 746 | } 747 | 748 | start, end, err := ts.cfg.findSelection(node) 749 | if err != nil { 750 | if !reflect.DeepEqual(ts.err.Error(), err.Error()) { 751 | t.Fatal(err) 752 | } 753 | return 754 | } 755 | 756 | ts.cfg.start = ts.cfg.fset.Position(start).Line 757 | ts.cfg.end = ts.cfg.fset.Position(end).Line 758 | 759 | err = ts.mod.Apply(ts.cfg.fset, node, start, end) 760 | if err != nil { 761 | if _, ok := err.(*modifytags.RewriteErrors); !ok { 762 | t.Fatal(err) 763 | } 764 | } 765 | 766 | var out string 767 | if err != nil { 768 | out, err = ts.cfg.format(node, err.(*modifytags.RewriteErrors)) 769 | } else { 770 | out, err = ts.cfg.format(node, nil) 771 | } 772 | 773 | if err != nil { 774 | t.Fatal(err) 775 | } 776 | got := []byte(out) 777 | 778 | // update golden file if necessary 779 | golden := filepath.Join(fixtureDir, fmt.Sprintf("%s.golden", ts.file)) 780 | if *update { 781 | err := ioutil.WriteFile(golden, got, 0644) 782 | if err != nil { 783 | t.Error(err) 784 | } 785 | return 786 | } 787 | 788 | // get golden file 789 | want, err := ioutil.ReadFile(golden) 790 | if err != nil { 791 | t.Fatal(err) 792 | } 793 | 794 | var from []byte 795 | if ts.cfg.modified != nil { 796 | from, err = ioutil.ReadAll(ts.cfg.modified) 797 | } else { 798 | from, err = ioutil.ReadFile(ts.cfg.file) 799 | } 800 | if err != nil { 801 | t.Fatal(err) 802 | } 803 | 804 | // compare 805 | if !bytes.Equal(got, want) { 806 | t.Errorf("case %s\ngot:\n====\n\n%s\nwant:\n=====\n\n%s\nfrom:\n=====\n\n%s\n", 807 | ts.file, got, want, from) 808 | } 809 | }) 810 | } 811 | } 812 | 813 | func TestJSON(t *testing.T) { 814 | test := []struct { 815 | cfg *config 816 | mod *modifytags.Modification 817 | file string 818 | err error 819 | }{ 820 | { 821 | file: "json_single", 822 | cfg: &config{ 823 | line: "5", 824 | }, 825 | mod: &modifytags.Modification{ 826 | Add: []string{"json"}, 827 | }, 828 | }, 829 | { 830 | file: "json_full", 831 | cfg: &config{ 832 | line: "4,6", 833 | }, 834 | mod: &modifytags.Modification{ 835 | Add: []string{"json"}, 836 | }, 837 | }, 838 | { 839 | file: "json_intersection", 840 | cfg: &config{ 841 | line: "5,16", 842 | }, 843 | mod: &modifytags.Modification{ 844 | Add: []string{"json"}, 845 | }, 846 | }, 847 | { 848 | // both small & end range larger than file 849 | file: "json_single", 850 | cfg: &config{ 851 | line: "30,32", //invalid selection 852 | }, 853 | mod: &modifytags.Modification{ 854 | Add: []string{"json"}, 855 | }, 856 | err: errors.New("line selection \"30,32\" is invalid: 30 is not within [0, 22]"), 857 | }, 858 | { 859 | // end range larger than file 860 | file: "json_single", 861 | cfg: &config{ 862 | line: "4,50", //invalid selection 863 | }, 864 | mod: &modifytags.Modification{ 865 | Add: []string{"json"}, 866 | }, 867 | err: errors.New("line selection \"4,50\" is invalid: 4 is not within [0, 22]"), 868 | }, 869 | { 870 | file: "json_errors", 871 | cfg: &config{ 872 | line: "4,7", 873 | }, 874 | mod: &modifytags.Modification{ 875 | Add: []string{"json"}, 876 | }, 877 | }, 878 | { 879 | file: "json_not_formatted", 880 | cfg: &config{ 881 | line: "3,4", 882 | }, 883 | mod: &modifytags.Modification{ 884 | Add: []string{"json"}, 885 | }, 886 | }, 887 | { 888 | file: "json_not_formatted_2", 889 | cfg: &config{ 890 | line: "3,3", 891 | }, 892 | mod: &modifytags.Modification{ 893 | Add: []string{"json"}, 894 | }, 895 | }, 896 | { 897 | file: "json_not_formatted_3", 898 | cfg: &config{ 899 | offset: 23, 900 | }, 901 | mod: &modifytags.Modification{ 902 | Add: []string{"json"}, 903 | }, 904 | }, 905 | { 906 | file: "json_not_formatted_4", 907 | cfg: &config{ 908 | offset: 51, 909 | }, 910 | mod: &modifytags.Modification{ 911 | Add: []string{"json"}, 912 | }, 913 | }, 914 | { 915 | file: "json_not_formatted_5", 916 | cfg: &config{ 917 | offset: 29, 918 | }, 919 | mod: &modifytags.Modification{ 920 | Add: []string{"json"}, 921 | }, 922 | }, 923 | { 924 | file: "json_not_formatted_6", 925 | cfg: &config{ 926 | line: "2,54", 927 | }, 928 | mod: &modifytags.Modification{ 929 | Add: []string{"json"}, 930 | }, 931 | }, 932 | { 933 | file: "json_all_structs", 934 | cfg: &config{ 935 | all: true, 936 | }, 937 | mod: &modifytags.Modification{ 938 | Add: []string{"json"}, 939 | }, 940 | }, 941 | } 942 | 943 | for _, ts := range test { 944 | t.Run(ts.file, func(t *testing.T) { 945 | ts.cfg.file = filepath.Join(fixtureDir, fmt.Sprintf("%s.input", ts.file)) 946 | // these are explicit and shouldn't be changed for this particular 947 | // main test 948 | ts.cfg.output = "json" 949 | ts.mod.Transform = modifytags.CamelCase 950 | 951 | node, err := ts.cfg.parse() 952 | if err != nil { 953 | t.Fatal(err) 954 | } 955 | 956 | start, end, err := ts.cfg.findSelection(node) 957 | 958 | ts.cfg.start = ts.cfg.fset.Position(start).Line 959 | ts.cfg.end = ts.cfg.fset.Position(end).Line 960 | 961 | if err != nil { 962 | if !reflect.DeepEqual(ts.err.Error(), err.Error()) { 963 | t.Fatal(err) 964 | } 965 | return 966 | } 967 | 968 | err = ts.mod.Apply(ts.cfg.fset, node, start, end) 969 | if err != nil { 970 | if _, ok := err.(*modifytags.RewriteErrors); !ok { 971 | t.Fatal(err) 972 | } 973 | } 974 | 975 | var out string 976 | if err != nil { 977 | out, err = ts.cfg.format(node, err.(*modifytags.RewriteErrors)) 978 | } else { 979 | out, err = ts.cfg.format(node, nil) 980 | } 981 | 982 | if err != nil && ts.err != nil && !reflect.DeepEqual(err.Error(), ts.err.Error()) { 983 | t.Logf("want: %v", ts.err) 984 | t.Logf("got: %v", err) 985 | t.Fatalf("unexpected error") 986 | } 987 | 988 | if ts.err != nil { 989 | return 990 | } 991 | 992 | got := []byte(out) 993 | 994 | // update golden file if necessary 995 | golden := filepath.Join(fixtureDir, fmt.Sprintf("%s.golden", ts.file)) 996 | if *update { 997 | err := ioutil.WriteFile(golden, got, 0644) 998 | if err != nil { 999 | t.Error(err) 1000 | } 1001 | return 1002 | } 1003 | 1004 | // get golden file 1005 | want, err := ioutil.ReadFile(golden) 1006 | if err != nil { 1007 | t.Fatal(err) 1008 | } 1009 | 1010 | from, err := ioutil.ReadFile(ts.cfg.file) 1011 | if err != nil { 1012 | t.Fatal(err) 1013 | } 1014 | 1015 | // compare 1016 | if !bytes.Equal(got, want) { 1017 | t.Errorf("case %s\ngot:\n====\n\n%s\nwant:\n=====\n\n%s\nfrom:\n=====\n\n%s\n", 1018 | ts.file, got, want, from) 1019 | } 1020 | }) 1021 | } 1022 | } 1023 | 1024 | func TestModifiedRewrite(t *testing.T) { 1025 | cfg := &config{ 1026 | output: "source", 1027 | structName: "foo", 1028 | file: "struct_add_modified", 1029 | modified: strings.NewReader(`struct_add_modified 1030 | 55 1031 | package foo 1032 | 1033 | type foo struct { 1034 | bar string 1035 | t bool 1036 | } 1037 | `), 1038 | } 1039 | 1040 | mod := &modifytags.Modification{ 1041 | Add: []string{"json"}, 1042 | Transform: modifytags.SnakeCase, 1043 | } 1044 | 1045 | node, err := cfg.parse() 1046 | if err != nil { 1047 | t.Fatal(err) 1048 | } 1049 | 1050 | start, end, err := cfg.findSelection(node) 1051 | if err != nil { 1052 | t.Fatal(err) 1053 | } 1054 | 1055 | cfg.start = cfg.fset.Position(start).Line 1056 | cfg.end = cfg.fset.Position(end).Line 1057 | 1058 | err = mod.Apply(cfg.fset, node, start, end) 1059 | if err != nil { 1060 | t.Fatal(err) 1061 | } 1062 | 1063 | var got string 1064 | if err != nil { 1065 | got, err = cfg.format(node, err.(*modifytags.RewriteErrors)) 1066 | } else { 1067 | got, err = cfg.format(node, nil) 1068 | } 1069 | 1070 | if err != nil { 1071 | t.Fatal(err) 1072 | } 1073 | 1074 | golden := filepath.Join(fixtureDir, "struct_add.golden") 1075 | want, err := ioutil.ReadFile(golden) 1076 | if err != nil { 1077 | t.Fatal(err) 1078 | } 1079 | 1080 | // compare 1081 | if !bytes.Equal([]byte(got), want) { 1082 | t.Errorf("got:\n====\n%s\nwant:\n====\n%s\n", got, want) 1083 | } 1084 | } 1085 | 1086 | func TestModifiedFileMissing(t *testing.T) { 1087 | cfg := &config{ 1088 | output: "source", 1089 | structName: "foo", 1090 | file: "struct_add_modified", 1091 | modified: strings.NewReader(`file_that_doesnt_exist 1092 | 55 1093 | package foo 1094 | 1095 | type foo struct { 1096 | bar string 1097 | t bool 1098 | } 1099 | `), 1100 | } 1101 | 1102 | _, err := cfg.parse() 1103 | if err == nil { 1104 | t.Fatal("expected error") 1105 | } 1106 | } 1107 | 1108 | func TestParseLines(t *testing.T) { 1109 | var tests = []struct { 1110 | file string 1111 | }{ 1112 | {file: "line_directive_unix"}, 1113 | {file: "line_directive_windows"}, 1114 | } 1115 | 1116 | for _, ts := range tests { 1117 | ts := ts 1118 | 1119 | t.Run(ts.file, func(t *testing.T) { 1120 | filePath := filepath.Join(fixtureDir, fmt.Sprintf("%s.input", ts.file)) 1121 | file, err := os.Open(filePath) 1122 | if err != nil { 1123 | t.Fatal(err) 1124 | } 1125 | defer file.Close() 1126 | 1127 | out, err := parseLines(file) 1128 | if err != nil { 1129 | t.Fatal(err) 1130 | } 1131 | 1132 | toBytes := func(Lines []string) []byte { 1133 | var buf bytes.Buffer 1134 | for _, Line := range Lines { 1135 | buf.WriteString(Line + "\n") 1136 | } 1137 | return buf.Bytes() 1138 | } 1139 | 1140 | got := toBytes(out) 1141 | 1142 | // update golden file if necessary 1143 | golden := filepath.Join(fixtureDir, fmt.Sprintf("%s.golden", ts.file)) 1144 | 1145 | if *update { 1146 | err := ioutil.WriteFile(golden, got, 0644) 1147 | if err != nil { 1148 | t.Error(err) 1149 | } 1150 | return 1151 | } 1152 | 1153 | // get golden file 1154 | want, err := ioutil.ReadFile(golden) 1155 | if err != nil { 1156 | t.Fatal(err) 1157 | } 1158 | 1159 | from, err := ioutil.ReadFile(filePath) 1160 | if err != nil { 1161 | t.Fatal(err) 1162 | } 1163 | 1164 | // compare 1165 | if !bytes.Equal(got, want) { 1166 | t.Errorf("case %s\ngot:\n====\n\n%s\nwant:\n=====\n\n%s\nfrom:\n=====\n\n%s\n", 1167 | ts.file, got, want, from) 1168 | } 1169 | 1170 | }) 1171 | } 1172 | } 1173 | -------------------------------------------------------------------------------- /modifytags/modifytags.go: -------------------------------------------------------------------------------- 1 | package modifytags 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "go/ast" 8 | "go/token" 9 | "sort" 10 | "strconv" 11 | "strings" 12 | "unicode" 13 | 14 | "github.com/fatih/camelcase" 15 | "github.com/fatih/structtag" 16 | ) 17 | 18 | // A Transform determines how Go field names will be translated into 19 | // names used in struct tags. For example, the [SnakeCase] transform 20 | // converts the field MyField into the json tag json:"my_field". 21 | type Transform int 22 | 23 | const ( 24 | SnakeCase = iota // MyField -> my_field 25 | CamelCase // MyField -> myField 26 | LispCase // MyField -> my-field 27 | PascalCase // MyField -> MyField 28 | TitleCase // MyField -> My Field 29 | Keep // keep the existing field name 30 | ) 31 | 32 | // A Modification defines how struct tags should be modified for a given input struct. 33 | type Modification struct { 34 | Add []string // tags to add 35 | AddOptions map[string][]string // options to add, per tag 36 | 37 | Remove []string // tags to remove 38 | RemoveOptions map[string][]string // options to remove, per tag 39 | 40 | Overwrite bool // if set, replace existing tags when adding 41 | SkipUnexportedFields bool // if set, do not modify tags on unexported struct fields 42 | 43 | Transform Transform // transform rule for adding tags 44 | Sort bool // if set, sort tags in ascending order by key name 45 | ValueFormat string // format for the tag's value, after transformation; for example "column:{field}" 46 | Clear bool // if set, clear all tags. tags are cleared before any new tags are added 47 | ClearOptions bool // if set, clear all tag options; options are cleared before any new options are added 48 | } 49 | 50 | // Apply applies the struct tag modifications of the receiver to all 51 | // struct fields contained within the given node between start and end position, modifying its input. 52 | func (mod *Modification) Apply(fset *token.FileSet, node ast.Node, start, end token.Pos) error { 53 | err := mod.validate() 54 | if err != nil { 55 | return err 56 | } 57 | 58 | return mod.rewrite(fset, node, start, end) 59 | } 60 | 61 | // rewrite rewrites the node for structs between the start and end positions 62 | func (mod *Modification) rewrite(fset *token.FileSet, node ast.Node, start, end token.Pos) error { 63 | var errs []error 64 | 65 | rewriteFunc := func(n ast.Node) bool { 66 | x, ok := n.(*ast.StructType) 67 | if !ok { 68 | return true 69 | } 70 | 71 | for _, f := range x.Fields.List { 72 | if !(start <= f.End() && f.Pos() <= end) { 73 | continue // not in range 74 | } 75 | 76 | fieldName := "" 77 | if len(f.Names) != 0 { 78 | for _, field := range f.Names { 79 | if !mod.SkipUnexportedFields || isPublicName(field.Name) { 80 | fieldName = field.Name 81 | break 82 | } 83 | } 84 | } 85 | 86 | // anonymous field 87 | if f.Names == nil { 88 | ident, ok := f.Type.(*ast.Ident) 89 | if !ok { 90 | continue 91 | } 92 | 93 | if !mod.SkipUnexportedFields { 94 | fieldName = ident.Name 95 | } 96 | } 97 | 98 | // nothing to process, continue with next field 99 | if fieldName == "" { 100 | continue 101 | } 102 | 103 | curTag := "" 104 | if f.Tag != nil { 105 | curTag = f.Tag.Value 106 | } 107 | 108 | res, err := mod.processField(fieldName, curTag) 109 | if err != nil { 110 | errs = append(errs, fmt.Errorf("%s:%d:%d:%s", 111 | fset.Position(f.Pos()).Filename, 112 | fset.Position(f.Pos()).Line, 113 | fset.Position(f.Pos()).Column, 114 | err)) 115 | continue 116 | } 117 | 118 | if res == "" { 119 | f.Tag = nil 120 | } else { 121 | if f.Tag == nil { 122 | f.Tag = &ast.BasicLit{} 123 | } 124 | f.Tag.Value = res 125 | } 126 | } 127 | 128 | return true 129 | } 130 | 131 | ast.Inspect(node, rewriteFunc) 132 | 133 | if len(errs) > 0 { 134 | return &RewriteErrors{Errs: errs} 135 | } 136 | return nil 137 | } 138 | 139 | // processField returns the new struct tag value for the given field 140 | func (mod *Modification) processField(fieldName, tagVal string) (string, error) { 141 | var tag string 142 | if tagVal != "" { 143 | var err error 144 | tag, err = strconv.Unquote(tagVal) 145 | if err != nil { 146 | return "", err 147 | } 148 | } 149 | 150 | tags, err := structtag.Parse(tag) 151 | if err != nil { 152 | return "", err 153 | } 154 | 155 | tags = mod.removeTags(tags) 156 | tags, err = mod.removeTagOptions(tags) 157 | if err != nil { 158 | return "", err 159 | } 160 | 161 | tags = mod.clearTags(tags) 162 | tags = mod.clearOptions(tags) 163 | 164 | tags, err = mod.addTags(fieldName, tags) 165 | if err != nil { 166 | return "", err 167 | } 168 | 169 | tags, err = mod.addTagOptions(tags) 170 | if err != nil { 171 | return "", err 172 | } 173 | 174 | if mod.Sort { 175 | sort.Sort(tags) 176 | } 177 | 178 | res := tags.String() 179 | if res != "" { 180 | res = quote(tags.String()) 181 | } 182 | 183 | return res, nil 184 | } 185 | 186 | func (mod *Modification) removeTags(tags *structtag.Tags) *structtag.Tags { 187 | if len(mod.Remove) == 0 { 188 | return tags 189 | } 190 | 191 | tags.Delete(mod.Remove...) 192 | return tags 193 | } 194 | 195 | func (mod *Modification) clearTags(tags *structtag.Tags) *structtag.Tags { 196 | if !mod.Clear { 197 | return tags 198 | } 199 | 200 | tags.Delete(tags.Keys()...) 201 | return tags 202 | } 203 | 204 | func (mod *Modification) clearOptions(tags *structtag.Tags) *structtag.Tags { 205 | if !mod.ClearOptions { 206 | return tags 207 | } 208 | 209 | for _, t := range tags.Tags() { 210 | t.Options = nil 211 | } 212 | 213 | return tags 214 | } 215 | 216 | func (mod *Modification) removeTagOptions(tags *structtag.Tags) (*structtag.Tags, error) { 217 | if len(mod.RemoveOptions) == 0 { 218 | return tags, nil 219 | } 220 | 221 | for key, val := range mod.RemoveOptions { 222 | for _, option := range val { 223 | tags.DeleteOptions(key, option) 224 | } 225 | } 226 | 227 | return tags, nil 228 | } 229 | 230 | func (mod *Modification) addTagOptions(tags *structtag.Tags) (*structtag.Tags, error) { 231 | if len(mod.AddOptions) == 0 { 232 | return tags, nil 233 | } 234 | 235 | for key, val := range mod.AddOptions { 236 | tags.AddOptions(key, val...) 237 | } 238 | 239 | return tags, nil 240 | } 241 | 242 | func (mod *Modification) addTags(fieldName string, tags *structtag.Tags) (*structtag.Tags, error) { 243 | if len(mod.Add) == 0 { 244 | return tags, nil 245 | } 246 | 247 | split := camelcase.Split(fieldName) 248 | name := "" 249 | 250 | switch mod.Transform { 251 | 252 | case LispCase: 253 | var lowerSplit []string 254 | for _, s := range split { 255 | lowerSplit = append(lowerSplit, strings.ToLower(s)) 256 | } 257 | 258 | name = strings.Join(lowerSplit, "-") 259 | case CamelCase: 260 | var titled []string 261 | for _, s := range split { 262 | titled = append(titled, strings.Title(s)) 263 | } 264 | 265 | titled[0] = strings.ToLower(titled[0]) 266 | 267 | name = strings.Join(titled, "") 268 | case PascalCase: 269 | var titled []string 270 | for _, s := range split { 271 | titled = append(titled, strings.Title(s)) 272 | } 273 | 274 | name = strings.Join(titled, "") 275 | case TitleCase: 276 | var titled []string 277 | for _, s := range split { 278 | titled = append(titled, strings.Title(s)) 279 | } 280 | 281 | name = strings.Join(titled, " ") 282 | case Keep: 283 | name = fieldName 284 | case SnakeCase: 285 | fallthrough 286 | default: 287 | // Use snakecase as the default. 288 | var lowerSplit []string 289 | for _, s := range split { 290 | s = strings.Trim(s, "_") 291 | if s == "" { 292 | continue 293 | } 294 | lowerSplit = append(lowerSplit, strings.ToLower(s)) 295 | } 296 | 297 | name = strings.Join(lowerSplit, "_") 298 | } 299 | 300 | if mod.ValueFormat != "" { 301 | prevName := name 302 | name = strings.ReplaceAll(mod.ValueFormat, "{field}", name) 303 | if name == mod.ValueFormat { 304 | // support old style for backward compatibility 305 | name = strings.ReplaceAll(mod.ValueFormat, "$field", prevName) 306 | } 307 | } 308 | 309 | for _, key := range mod.Add { 310 | split = strings.SplitN(key, ":", 2) 311 | if len(split) >= 2 { 312 | key = split[0] 313 | name = strings.Join(split[1:], "") 314 | } 315 | 316 | tag, err := tags.Get(key) 317 | if err != nil { 318 | // tag doesn't exist, create a new one 319 | tag = &structtag.Tag{ 320 | Key: key, 321 | Name: name, 322 | } 323 | } else if mod.Overwrite { 324 | tag.Name = name 325 | } 326 | 327 | if err := tags.Set(tag); err != nil { 328 | return nil, err 329 | } 330 | } 331 | 332 | return tags, nil 333 | } 334 | 335 | func isPublicName(name string) bool { 336 | for _, c := range name { 337 | return unicode.IsUpper(c) 338 | } 339 | return false 340 | } 341 | 342 | // validate determines whether the Modification is valid or not. 343 | func (mod *Modification) validate() error { 344 | if len(mod.Add) == 0 && 345 | len(mod.AddOptions) == 0 && 346 | !mod.Clear && 347 | !mod.ClearOptions && 348 | len(mod.RemoveOptions) == 0 && 349 | len(mod.Remove) == 0 { 350 | return errors.New("one of " + 351 | "[-add-tags, -add-options, -remove-tags, -remove-options, -clear-tags, -clear-options]" + 352 | " should be defined") 353 | } 354 | return nil 355 | } 356 | 357 | func quote(tag string) string { 358 | return "`" + tag + "`" 359 | } 360 | 361 | // RewriteErrors are errors that occurred while rewriting struct field tags. 362 | type RewriteErrors struct { 363 | Errs []error 364 | } 365 | 366 | func (r *RewriteErrors) Error() string { 367 | var buf bytes.Buffer 368 | for _, e := range r.Errs { 369 | buf.WriteString(fmt.Sprintf("%s\n", e.Error())) 370 | } 371 | return buf.String() 372 | } 373 | -------------------------------------------------------------------------------- /modifytags/modifytags_test.go: -------------------------------------------------------------------------------- 1 | package modifytags 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "go/format" 8 | "go/parser" 9 | "go/token" 10 | "io/ioutil" 11 | "os" 12 | "path/filepath" 13 | "testing" 14 | ) 15 | 16 | var update = flag.Bool("update", false, "update golden (.out) files") 17 | 18 | // This is the directory where our test fixtures are. 19 | const fixtureDir = "./test-fixtures" 20 | 21 | func TestApply(t *testing.T) { 22 | var tests = []struct { 23 | file string 24 | m *Modification 25 | start token.Pos 26 | end token.Pos 27 | }{ 28 | { 29 | file: "all_structs", 30 | m: &Modification{ 31 | Add: []string{"json"}, 32 | }, 33 | start: token.NoPos, // will be set to start of file 34 | end: token.NoPos, // will be set to end of file 35 | }, 36 | { 37 | file: "clear_all_tags", 38 | m: &Modification{ 39 | Clear: true, 40 | }, 41 | start: token.NoPos, // will be set to start of file 42 | end: token.NoPos, // will be set to end of file 43 | }, 44 | { 45 | file: "remove_some_tags", 46 | m: &Modification{ 47 | Remove: []string{"json"}, 48 | }, 49 | start: token.NoPos, // will be set to start of file 50 | end: token.NoPos, // will be set to end of file 51 | }, 52 | { 53 | file: "add_tags_pos_between_line", 54 | m: &Modification{ 55 | Add: []string{"json"}, 56 | AddOptions: map[string][]string{ 57 | "json": {"omitempty"}, 58 | }, 59 | }, 60 | // TODO: use markers in the test content to delineate start and end, rather than hard-coding here. 61 | start: token.Pos(50), // middle of second struct field 62 | end: token.Pos(200), // middle of fourth struct field 63 | }, 64 | } 65 | 66 | for _, ts := range tests { 67 | ts := ts 68 | 69 | t.Run(ts.file, func(t *testing.T) { 70 | filePath := filepath.Join(fixtureDir, fmt.Sprintf("%s.input", ts.file)) 71 | file, err := os.Open(filePath) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | defer file.Close() 76 | 77 | fset := token.NewFileSet() 78 | 79 | node, err := parser.ParseFile(fset, filepath.Join(fixtureDir, fmt.Sprintf("%s.input", ts.file)), nil, parser.ParseComments) 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | 84 | if ts.start == token.NoPos { 85 | ts.start = node.Pos() 86 | } 87 | if ts.end == token.NoPos { 88 | ts.end = node.End() 89 | } 90 | 91 | err = ts.m.Apply(fset, node, ts.start, ts.end) 92 | if err != nil { 93 | t.Fatal(err) 94 | } 95 | 96 | var got bytes.Buffer 97 | err = format.Node(&got, fset, node) 98 | if err != nil { 99 | t.Fatal(err) 100 | } 101 | 102 | // update golden file if necessary 103 | golden := filepath.Join(fixtureDir, fmt.Sprintf("%s.golden", ts.file)) 104 | 105 | if *update { 106 | err := ioutil.WriteFile(golden, got.Bytes(), 0644) 107 | if err != nil { 108 | t.Error(err) 109 | } 110 | return 111 | } 112 | 113 | // get golden file 114 | want, err := ioutil.ReadFile(golden) 115 | if err != nil { 116 | t.Fatal(err) 117 | } 118 | 119 | from, err := ioutil.ReadFile(filePath) 120 | if err != nil { 121 | t.Fatal(err) 122 | } 123 | 124 | // compare 125 | if !bytes.Equal(got.Bytes(), want) { 126 | t.Errorf("case %s\ngot:\n====\n\n%s\nwant:\n=====\n\n%s\nfrom:\n=====\n\n%s\n", 127 | ts.file, got.Bytes(), want, from) 128 | } 129 | 130 | }) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /modifytags/test-fixtures/add_tags_pos_between_line.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | t bool `json:"t,omitempty"` 6 | qux string `json:"qux,omitempty"` 7 | yoo string `json:"yoo,omitempty"` 8 | } 9 | -------------------------------------------------------------------------------- /modifytags/test-fixtures/add_tags_pos_between_line.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | t bool 6 | qux string 7 | yoo string 8 | } 9 | -------------------------------------------------------------------------------- /modifytags/test-fixtures/all_structs.golden: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type foo struct { 4 | bar string `json:"bar"` 5 | MyExample bool `json:"my_example"` 6 | MyAnother []string `json:"my_another"` 7 | } 8 | 9 | const exampleVar = "foo" 10 | 11 | type bar struct { 12 | // loose comment 13 | 14 | // home 15 | ankara string `json:"ankara"` 16 | yeap bool `json:"yeap"` // just a boolean 17 | 18 | // great cities 19 | cities []string `json:"cities"` 20 | 21 | // seconed loose comment 22 | } 23 | -------------------------------------------------------------------------------- /modifytags/test-fixtures/all_structs.input: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type foo struct { 4 | bar string 5 | MyExample bool 6 | MyAnother []string 7 | } 8 | 9 | const exampleVar = "foo" 10 | 11 | type bar struct { 12 | // loose comment 13 | 14 | // home 15 | ankara string 16 | yeap bool // just a boolean 17 | 18 | // great cities 19 | cities []string 20 | 21 | // seconed loose comment 22 | } 23 | -------------------------------------------------------------------------------- /modifytags/test-fixtures/clear_all_tags.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | t bool 6 | t bool 7 | qux string 8 | yoo string 9 | } 10 | -------------------------------------------------------------------------------- /modifytags/test-fixtures/clear_all_tags.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar,omitempty" hcl:"bar,omitnested"` 5 | t bool `hcl:"t"` 6 | t bool `hcl:"t,omitempty"` 7 | qux string `json:"qux,omitempty" hcl:"qux,squash,keys"` 8 | yoo string `json:"yoo"` 9 | } 10 | -------------------------------------------------------------------------------- /modifytags/test-fixtures/remove_some_tags.golden: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type foo struct { 4 | bar string `hcl:"bar,omitnested"` 5 | t bool `hcl:"t"` 6 | t bool `hcl:"t,omitempty"` 7 | qux string `hcl:"qux,squash,keys"` 8 | yoo string 9 | } 10 | -------------------------------------------------------------------------------- /modifytags/test-fixtures/remove_some_tags.input: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type foo struct { 4 | bar string `json:"bar,omitempty" hcl:"bar,omitnested"` 5 | t bool `hcl:"t"` 6 | t bool `hcl:"t,omitempty"` 7 | qux string `json:"qux,omitempty" hcl:"qux,squash,keys"` 8 | yoo string `json:"yoo"` 9 | } 10 | -------------------------------------------------------------------------------- /test-fixtures/all_structs.golden: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type foo struct { 4 | bar string `json:"bar"` 5 | MyExample bool `json:"my_example"` 6 | MyAnother []string `json:"my_another"` 7 | } 8 | 9 | const exampleVar = "foo" 10 | 11 | type bar struct { 12 | // loose comment 13 | 14 | // home 15 | ankara string `json:"ankara"` 16 | yeap bool `json:"yeap"` // just a boolean 17 | 18 | // great cities 19 | cities []string `json:"cities"` 20 | 21 | // seconed loose comment 22 | } 23 | -------------------------------------------------------------------------------- /test-fixtures/all_structs.input: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type foo struct { 4 | bar string 5 | MyExample bool 6 | MyAnother []string 7 | } 8 | 9 | const exampleVar = "foo" 10 | 11 | type bar struct { 12 | // loose comment 13 | 14 | // home 15 | ankara string 16 | yeap bool // just a boolean 17 | 18 | // great cities 19 | cities []string 20 | 21 | // seconed loose comment 22 | } 23 | -------------------------------------------------------------------------------- /test-fixtures/empty_file.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | -------------------------------------------------------------------------------- /test-fixtures/empty_file.input: -------------------------------------------------------------------------------- 1 | package foo 2 | -------------------------------------------------------------------------------- /test-fixtures/errors.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar"` 5 | t bool `json:"malformed` 6 | a []string `json:"a"` 7 | b bool `json:"b"` 8 | } 9 | -------------------------------------------------------------------------------- /test-fixtures/errors.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | t bool `json:"malformed` 6 | a []string 7 | b bool 8 | } 9 | -------------------------------------------------------------------------------- /test-fixtures/field_add.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar"` 5 | qaz, qux string 6 | timestamp time.Time 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/field_add.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | qaz, qux string 6 | timestamp time.Time 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/field_add_existing.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar"` 5 | baz string 6 | timestamp time.Time `json:"@timestamp"` 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/field_add_existing.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | baz string 6 | timestamp time.Time `json:"@timestamp"` 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/field_add_same_line.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | qaz, qux string `json:"qaz"` 6 | timestamp time.Time 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/field_add_same_line.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | qaz, qux string 6 | timestamp time.Time 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/field_clear_options.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar" hcl:"bar"` 5 | t bool `hcl:"t"` 6 | t bool `hcl:"t,omitempty"` 7 | qux string `json:"qux,omitempty" hcl:"qux,squash,keys"` 8 | yoo string `json:"yoo"` 9 | } 10 | -------------------------------------------------------------------------------- /test-fixtures/field_clear_options.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar,omitempty" hcl:"bar,omitnested"` 5 | t bool `hcl:"t"` 6 | t bool `hcl:"t,omitempty"` 7 | qux string `json:"qux,omitempty" hcl:"qux,squash,keys"` 8 | yoo string `json:"yoo"` 9 | } 10 | -------------------------------------------------------------------------------- /test-fixtures/field_clear_tags.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | t bool `hcl:"t"` 6 | t bool `hcl:"t,omitempty"` 7 | qux string `json:"qux,omitempty" hcl:"qux,squash,keys"` 8 | yoo string `json:"yoo"` 9 | } 10 | -------------------------------------------------------------------------------- /test-fixtures/field_clear_tags.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar,omitempty" hcl:"bar,omitnested"` 5 | t bool `hcl:"t"` 6 | t bool `hcl:"t,omitempty"` 7 | qux string `json:"qux,omitempty" hcl:"qux,squash,keys"` 8 | yoo string `json:"yoo"` 9 | } 10 | -------------------------------------------------------------------------------- /test-fixtures/field_remove.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | baz string `json:"baz"` 6 | t bool `hcl:"t"` 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/field_remove.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar"` 5 | baz string `json:"baz"` 6 | t bool `hcl:"t"` 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/json_all_structs.golden: -------------------------------------------------------------------------------- 1 | { 2 | "start": 1, 3 | "end": 22, 4 | "lines": [ 5 | "package main", 6 | "", 7 | "type foo struct {", 8 | "\tbar string `json:\"bar\"`", 9 | "\tMyExample bool `json:\"myExample\"`", 10 | "\tMyAnother []string `json:\"myAnother\"`", 11 | "}", 12 | "", 13 | "const exampleVar = \"foo\"", 14 | "", 15 | "type bar struct {", 16 | "\t// loose comment", 17 | "", 18 | "\t// home", 19 | "\tankara string `json:\"ankara\"`", 20 | "\tyeap bool `json:\"yeap\"` // just a boolean", 21 | "", 22 | "\t// great cities", 23 | "\tcities []string `json:\"cities\"`", 24 | "", 25 | "\t// seconed loose comment", 26 | "}" 27 | ] 28 | } -------------------------------------------------------------------------------- /test-fixtures/json_all_structs.input: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type foo struct { 4 | bar string 5 | MyExample bool 6 | MyAnother []string 7 | } 8 | 9 | const exampleVar = "foo" 10 | 11 | type bar struct { 12 | // loose comment 13 | 14 | // home 15 | ankara string 16 | yeap bool // just a boolean 17 | 18 | // great cities 19 | cities []string 20 | 21 | // seconed loose comment 22 | } 23 | -------------------------------------------------------------------------------- /test-fixtures/json_errors.golden: -------------------------------------------------------------------------------- 1 | { 2 | "start": 4, 3 | "end": 7, 4 | "lines": [ 5 | "\tbar string `json:\"bar\"`", 6 | "\tt bool `json`", 7 | "\ta []string `json:\"a\"`", 8 | "\tb bool `json:\"b\"`" 9 | ], 10 | "errors": [ 11 | "test-fixtures/json_errors.input:5:2:bad syntax for struct tag pair" 12 | ] 13 | } -------------------------------------------------------------------------------- /test-fixtures/json_errors.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | t bool `json` 6 | a []string 7 | b bool 8 | } 9 | -------------------------------------------------------------------------------- /test-fixtures/json_full.golden: -------------------------------------------------------------------------------- 1 | { 2 | "start": 4, 3 | "end": 6, 4 | "lines": [ 5 | "\tbar string `json:\"bar\"`", 6 | "\tMyExample bool `json:\"myExample\"`", 7 | "\tMyAnother []string `json:\"myAnother\"`" 8 | ] 9 | } -------------------------------------------------------------------------------- /test-fixtures/json_full.input: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type foo struct { 4 | bar string 5 | MyExample bool 6 | MyAnother []string 7 | } 8 | 9 | const exampleVar = "foo" 10 | 11 | type bar struct { 12 | // loose comment 13 | 14 | // home 15 | ankara string 16 | yeap bool // just a boolean 17 | 18 | // great cities 19 | cities []string 20 | 21 | // seconed loose comment 22 | } 23 | -------------------------------------------------------------------------------- /test-fixtures/json_intersection.golden: -------------------------------------------------------------------------------- 1 | { 2 | "start": 5, 3 | "end": 16, 4 | "lines": [ 5 | "\tMyExample bool `json:\"myExample\"`", 6 | "\tMyAnother []string `json:\"myAnother\"`", 7 | "}", 8 | "", 9 | "const exampleVar = \"foo\"", 10 | "", 11 | "type bar struct {", 12 | "\t// loose comment", 13 | "", 14 | "\t// home", 15 | "\tankara string `json:\"ankara\"`", 16 | "\tyeap bool `json:\"yeap\"` // just a boolean" 17 | ] 18 | } -------------------------------------------------------------------------------- /test-fixtures/json_intersection.input: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type foo struct { 4 | bar string 5 | MyExample bool 6 | MyAnother []string 7 | } 8 | 9 | const exampleVar = "foo" 10 | 11 | type bar struct { 12 | // loose comment 13 | 14 | // home 15 | ankara string 16 | yeap bool // just a boolean 17 | 18 | // great cities 19 | cities []string 20 | 21 | // seconed loose comment 22 | } 23 | -------------------------------------------------------------------------------- /test-fixtures/json_not_formatted.golden: -------------------------------------------------------------------------------- 1 | { 2 | "start": 3, 3 | "end": 4, 4 | "lines": [ 5 | "\tFoo int `json:\"foo\"`", 6 | "\tbar int `json:\"bar\"`" 7 | ] 8 | } -------------------------------------------------------------------------------- /test-fixtures/json_not_formatted.input: -------------------------------------------------------------------------------- 1 | package a 2 | type x struct { 3 | Foo int 4 | bar int 5 | } 6 | -------------------------------------------------------------------------------- /test-fixtures/json_not_formatted_2.golden: -------------------------------------------------------------------------------- 1 | { 2 | "start": 3, 3 | "end": 3, 4 | "lines": [ 5 | "\tFoo int `json:\"foo\"`" 6 | ] 7 | } -------------------------------------------------------------------------------- /test-fixtures/json_not_formatted_2.input: -------------------------------------------------------------------------------- 1 | package a 2 | type x struct { 3 | Foo int 4 | bar int 5 | } 6 | -------------------------------------------------------------------------------- /test-fixtures/json_not_formatted_3.golden: -------------------------------------------------------------------------------- 1 | { 2 | "start": 2, 3 | "end": 5, 4 | "lines": [ 5 | "type x struct {", 6 | "\tFoo int `json:\"foo\"`", 7 | "\tbar int `json:\"bar\"`", 8 | "}" 9 | ] 10 | } -------------------------------------------------------------------------------- /test-fixtures/json_not_formatted_3.input: -------------------------------------------------------------------------------- 1 | package a 2 | type x struct { 3 | Foo int 4 | bar int 5 | } 6 | -------------------------------------------------------------------------------- /test-fixtures/json_not_formatted_4.golden: -------------------------------------------------------------------------------- 1 | { 2 | "start": 2, 3 | "end": 6, 4 | "lines": [ 5 | "type x struct {", 6 | "\tFoo int `json:\"foo\"`", 7 | "\tbar int `json:\"bar\"`", 8 | "", 9 | "}" 10 | ] 11 | } -------------------------------------------------------------------------------- /test-fixtures/json_not_formatted_4.input: -------------------------------------------------------------------------------- 1 | package a 2 | type x struct { 3 | Foo int 4 | bar int 5 | 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/json_not_formatted_5.golden: -------------------------------------------------------------------------------- 1 | { 2 | "start": 3, 3 | "end": 12, 4 | "lines": [ 5 | "type x struct {", 6 | "", 7 | "", 8 | "", 9 | "", 10 | "", 11 | " Foo int `json:\"foo\"`", 12 | "\tbar string `json:\"bar\"`", 13 | "", 14 | "}" 15 | ] 16 | } -------------------------------------------------------------------------------- /test-fixtures/json_not_formatted_5.input: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | type x struct { 4 | 5 | 6 | 7 | 8 | 9 | Foo int 10 | bar string 11 | 12 | } 13 | -------------------------------------------------------------------------------- /test-fixtures/json_not_formatted_6.golden: -------------------------------------------------------------------------------- 1 | { 2 | "start": 2, 3 | "end": 54, 4 | "lines": [ 5 | "", 6 | "type x struct {", 7 | "\tFoo int `json:\"foo\"`", 8 | "\tbar string `json:\"bar\"`", 9 | "", 10 | "", 11 | "", 12 | "", 13 | "", 14 | "", 15 | "", 16 | "}", 17 | "", 18 | "func main() {", 19 | "", 20 | "", 21 | "", 22 | "", 23 | " fmt.Println(\"---\")", 24 | "\tfmt.Println(\"---\")", 25 | "\tfmt.Println(\"---\")", 26 | "\tfmt.Println(\"zeynep\")", 27 | "\tfmt.Println(\"alper\")", 28 | "\tfmt.Println(\"miray\")", 29 | "\tfmt.Println(\"---\")", 30 | "\tfmt.Println(\"---\")", 31 | "\tfmt.Println(\"---\")", 32 | "", 33 | "", 34 | "", 35 | "}", 36 | "", 37 | "type b struct {", 38 | "\tFoo int `json:\"foo\"`", 39 | "\tbar string `json:\"bar\"`", 40 | "", 41 | "}", 42 | "", 43 | "type b struct {", 44 | "", 45 | "", 46 | "", 47 | "", 48 | "", 49 | "", 50 | "", 51 | "", 52 | "", 53 | "", 54 | " Foo int `json:\"foo\"`", 55 | "\tbar string `json:\"bar\"`", 56 | "", 57 | "}" 58 | ] 59 | } -------------------------------------------------------------------------------- /test-fixtures/json_not_formatted_6.input: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type x struct { 4 | Foo int 5 | bar string 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | } 14 | 15 | func main() { 16 | 17 | 18 | 19 | 20 | fmt.Println("---") 21 | fmt.Println("---") 22 | fmt.Println("---") 23 | fmt.Println("zeynep") 24 | fmt.Println("alper") 25 | fmt.Println("miray") 26 | fmt.Println("---") 27 | fmt.Println("---") 28 | fmt.Println("---") 29 | 30 | 31 | 32 | } 33 | 34 | type b struct { 35 | Foo int 36 | bar string 37 | 38 | } 39 | 40 | type b struct { 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | Foo int 52 | bar string 53 | 54 | } 55 | -------------------------------------------------------------------------------- /test-fixtures/json_single.golden: -------------------------------------------------------------------------------- 1 | { 2 | "start": 5, 3 | "end": 5, 4 | "lines": [ 5 | "\tMyExample bool `json:\"myExample\"`" 6 | ] 7 | } -------------------------------------------------------------------------------- /test-fixtures/json_single.input: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type foo struct { 4 | bar string 5 | MyExample bool 6 | MyAnother []string 7 | } 8 | 9 | const exampleVar = "foo" 10 | 11 | type bar struct { 12 | // loose comment 13 | 14 | // home 15 | ankara string 16 | yeap bool // just a boolean 17 | 18 | // great cities 19 | cities []string 20 | 21 | // seconed loose comment 22 | } 23 | -------------------------------------------------------------------------------- /test-fixtures/line_add.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar"` 5 | t bool 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/line_add.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | t bool 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/line_add_comment.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | // a comment about b 5 | 6 | bar string `json:"bar"` 7 | t bool `json:"t"` 8 | 9 | // a comment about b 10 | // b []string 11 | } 12 | -------------------------------------------------------------------------------- /test-fixtures/line_add_comment.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | // a comment about b 5 | 6 | bar string 7 | t bool 8 | 9 | // a comment about b 10 | // b []string 11 | } 12 | -------------------------------------------------------------------------------- /test-fixtures/line_add_intersect_partial.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | t bool `json:"t"` 6 | qux int `json:"qux"` 7 | } 8 | 9 | type bar struct { 10 | foo string `json:"foo"` 11 | s bool `json:"s"` 12 | mut int 13 | } 14 | -------------------------------------------------------------------------------- /test-fixtures/line_add_intersect_partial.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | t bool 6 | qux int 7 | } 8 | 9 | type bar struct { 10 | foo string 11 | s bool 12 | mut int 13 | } 14 | 15 | -------------------------------------------------------------------------------- /test-fixtures/line_add_multiple_option.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar,omitempty" hcl:"bar,squash"` 5 | f bool `hcl:"f,squash"` 6 | t bool `hcl:"t,squash"` 7 | Ankara []string `json:"ankara,omitempty" hcl:"ankara,squash"` 8 | a []string `json:"a"` 9 | } 10 | -------------------------------------------------------------------------------- /test-fixtures/line_add_multiple_option.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar"` 5 | f bool 6 | t bool 7 | Ankara []string `json:"ankara"` 8 | a []string `json:"a"` 9 | } 10 | -------------------------------------------------------------------------------- /test-fixtures/line_add_no_override.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"myBar"` 5 | t bool `json:"t"` 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/line_add_no_override.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"myBar"` 5 | t bool 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/line_add_option.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar,omitempty"` 5 | f bool 6 | t bool 7 | Ankara []string `json:"ankara,omitempty"` 8 | a []string `json:"a"` 9 | } 10 | -------------------------------------------------------------------------------- /test-fixtures/line_add_option.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar"` 5 | f bool 6 | t bool 7 | Ankara []string `json:"ankara"` 8 | a []string `json:"a"` 9 | } 10 | -------------------------------------------------------------------------------- /test-fixtures/line_add_option_existing.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar"` 5 | f bool 6 | t bool 7 | Ankara []string `json:"ankara,omitempty"` 8 | timestamp time.Time `json:"@timestamp,omitempty"` 9 | } 10 | -------------------------------------------------------------------------------- /test-fixtures/line_add_option_existing.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar"` 5 | f bool 6 | t bool 7 | Ankara []string `json:"ankara"` 8 | timestamp time.Time `json:"@timestamp"` 9 | } 10 | -------------------------------------------------------------------------------- /test-fixtures/line_add_option_with_equal.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `validate:"bar,max=32"` 5 | t bool `json:"t" validate:"t,max=32"` 6 | qux int `json:"qux" validate:"qux,max=32"` 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/line_add_option_with_equal.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | t bool `json:"t"` 6 | qux int `json:"qux"` 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/line_add_outside.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar"` 5 | t bool `json:"t"` 6 | qux int `json:"qux"` 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/line_add_outside.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | t bool 6 | qux int 7 | } 8 | 9 | -------------------------------------------------------------------------------- /test-fixtures/line_add_outside_partial_end.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | t bool `json:"t"` 6 | qux int `json:"qux"` 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/line_add_outside_partial_end.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | t bool 6 | qux int 7 | } 8 | 9 | -------------------------------------------------------------------------------- /test-fixtures/line_add_outside_partial_start.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar"` 5 | t bool `json:"t"` 6 | qux int 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/line_add_outside_partial_start.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | t bool 6 | qux int 7 | } 8 | 9 | -------------------------------------------------------------------------------- /test-fixtures/line_add_override.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar"` 5 | t bool `json:"t"` 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/line_add_override.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"myBar"` 5 | t bool 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/line_add_override_column.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"MyBar:bar"` 5 | baz string `json:"baz"` 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/line_add_override_column.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar"` 5 | baz string `json:"baz"` 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/line_add_override_mixed_column_and_equal.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"MyBar:bar:foo=qux"` 5 | baz string `json:"baz"` 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/line_add_override_mixed_column_and_equal.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar"` 5 | baz string `json:"baz"` 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/line_add_override_multi_column.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"MyBar:bar:foo"` 5 | baz string `json:"baz"` 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/line_add_override_multi_column.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar"` 5 | baz string `json:"baz"` 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/line_add_override_multi_equal.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"MyBar=bar=foo"` 5 | baz string `json:"baz"` 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/line_add_override_multi_equal.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar"` 5 | baz string `json:"baz"` 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/line_camelcase_add.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar"` 5 | MyExample bool `json:"myExample"` 6 | MyAnother []string 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/line_camelcase_add.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | MyExample bool 6 | MyAnother []string 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/line_camelcase_add_embedded.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar `json:"bar"` 5 | MyExample bool `json:"myExample"` 6 | MyAnother []string `json:"myAnother"` 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/line_camelcase_add_embedded.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar 5 | MyExample bool 6 | MyAnother []string 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/line_directive_unix.golden: -------------------------------------------------------------------------------- 1 | package a 2 | type x struct { 3 | Foo int `json:"foo"` 4 | bar int 5 | } 6 | -------------------------------------------------------------------------------- /test-fixtures/line_directive_unix.input: -------------------------------------------------------------------------------- 1 | //line /Users/fatih/test.go:1 2 | package a 3 | 4 | //line /Users/fatih/test.go:2 5 | type x struct { 6 | Foo int `json:"foo"` 7 | bar int 8 | } 9 | -------------------------------------------------------------------------------- /test-fixtures/line_directive_windows.golden: -------------------------------------------------------------------------------- 1 | package a 2 | type x struct { 3 | Foo int `json:"foo"` 4 | bar int 5 | } 6 | -------------------------------------------------------------------------------- /test-fixtures/line_directive_windows.input: -------------------------------------------------------------------------------- 1 | //line c:\\file\\path\\to\\file.go:1 2 | package a 3 | 4 | //line c:\\file\\path\\to\\file.go:2 5 | type x struct { 6 | Foo int `json:"foo"` 7 | bar int 8 | } 9 | -------------------------------------------------------------------------------- /test-fixtures/line_lispcase_add.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar"` 5 | MyExample bool `json:"my-example"` 6 | MyAnother []string `json:"my-another"` 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/line_lispcase_add.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | MyExample bool 6 | MyAnother []string 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/line_multiple_add.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | t bool `json:"t"` 6 | a []string `json:"a"` 7 | b bool 8 | } 9 | -------------------------------------------------------------------------------- /test-fixtures/line_multiple_add.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | t bool 6 | a []string 7 | b bool 8 | } 9 | -------------------------------------------------------------------------------- /test-fixtures/line_pascalcase_add.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"Bar"` 5 | MyExample bool `json:"MyExample"` 6 | MyAnother []string 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/line_pascalcase_add.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | MyExample bool 6 | MyAnother []string 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/line_pascalcase_add_embedded.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar `json:"Bar"` 5 | MyExample bool `json:"MyExample"` 6 | MyAnother []string `json:"MyAnother"` 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/line_pascalcase_add_embedded.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar 5 | MyExample bool 6 | MyAnother []string 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/line_remove.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar"` 5 | t bool `hcl:"t"` 6 | qux string 7 | yoo string 8 | } 9 | -------------------------------------------------------------------------------- /test-fixtures/line_remove.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar"` 5 | t bool `hcl:"t"` 6 | qux string `json:"qux"` 7 | yoo string `json:"yoo"` 8 | } 9 | -------------------------------------------------------------------------------- /test-fixtures/line_remove_option.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar,omitempty" hcl:"bar,omitnested"` 5 | t bool `hcl:"t"` 6 | t bool `hcl:"t,omitempty"` 7 | qux string `json:"qux,omitempty" hcl:"qux,keys"` 8 | yoo string `json:"yoo"` 9 | } 10 | -------------------------------------------------------------------------------- /test-fixtures/line_remove_option.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar,omitempty" hcl:"bar,omitnested"` 5 | t bool `hcl:"t"` 6 | t bool `hcl:"t,omitempty"` 7 | qux string `json:"qux,omitempty" hcl:"qux,squash,keys"` 8 | yoo string `json:"yoo"` 9 | } 10 | 11 | -------------------------------------------------------------------------------- /test-fixtures/line_remove_option_with_equal.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `validate:"bar"` 5 | t bool `json:"t" validate:"t"` 6 | qux int `json:"qux" validate:"qux"` 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/line_remove_option_with_equal.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `validate:"bar,max=32"` 5 | t bool `json:"t" validate:"t,max=32"` 6 | qux int `json:"qux" validate:"qux,max=32"` 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/line_remove_options.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar" hcl:"bar"` 5 | t bool `hcl:"t"` 6 | t bool `hcl:"t,omitempty"` 7 | qux string `json:"qux" hcl:"qux,squash,keys"` 8 | yoo string `json:"yoo"` 9 | } 10 | -------------------------------------------------------------------------------- /test-fixtures/line_remove_options.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar,omitempty" hcl:"bar,omitnested"` 5 | t bool `hcl:"t"` 6 | t bool `hcl:"t,omitempty"` 7 | qux string `json:"qux,omitempty" hcl:"qux,squash,keys"` 8 | yoo string `json:"yoo"` 9 | } 10 | 11 | -------------------------------------------------------------------------------- /test-fixtures/line_titlecase_add.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"Bar"` 5 | MyExample bool `json:"My Example"` 6 | MyAnother []string `json:"My Another"` 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/line_titlecase_add.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | MyExample bool 6 | MyAnother []string 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/line_titlecase_add_embedded.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar `json:"Bar"` 5 | MyExample bool `json:"My Example"` 6 | MyAnother []string `json:"My Another"` 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/line_titlecase_add_embedded.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar 5 | MyExample bool 6 | MyAnother []string 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/line_value_add.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"foo"` 5 | MyExample bool `json:"foo"` 6 | MyAnother []string `json:"foo"` 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/line_value_add.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | MyExample bool 6 | MyAnother []string 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/not_formatted.golden: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | type x struct { 4 | Foo int `json:"foo"` 5 | bar int `json:"bar"` 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/not_formatted.input: -------------------------------------------------------------------------------- 1 | package a 2 | type x struct { 3 | Foo int 4 | bar int 5 | } 6 | -------------------------------------------------------------------------------- /test-fixtures/offset_add.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar"` 5 | t bool `json:"t"` 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/offset_add.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | t bool 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/offset_add_composite.golden: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | &struct { 5 | Hello string `json:"hello"` 6 | }{} 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/offset_add_composite.input: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | &struct { 5 | Hello string 6 | }{} 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/offset_add_duplicate.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | // NOTE(arslan): this is a valid Go syntax file, but not a valid compilable 4 | // file. Even in that case we should allow things to be modified as we just 5 | // care about the AST. 6 | 7 | type foo struct { 8 | bar string `json:"bar"` 9 | t bool `json:"t"` 10 | } 11 | 12 | type foo struct { 13 | bar string 14 | t bool 15 | } 16 | -------------------------------------------------------------------------------- /test-fixtures/offset_add_duplicate.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | // NOTE(arslan): this is a valid Go syntax file, but not a valid compilable 4 | // file. Even in that case we should allow things to be modified as we just 5 | // care about the AST. 6 | 7 | type foo struct { 8 | bar string 9 | t bool 10 | } 11 | 12 | type foo struct { 13 | bar string 14 | t bool 15 | } 16 | -------------------------------------------------------------------------------- /test-fixtures/offset_add_literal_in.golden: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | var in struct { 5 | Foo string `json:"foo"` 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/offset_add_literal_in.input: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | var in struct { 5 | Foo string 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/offset_add_literal_out.golden: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var out struct { 4 | Foo string `json:"foo"` 5 | } 6 | -------------------------------------------------------------------------------- /test-fixtures/offset_add_literal_out.input: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var out struct { 4 | Foo string 5 | } 6 | -------------------------------------------------------------------------------- /test-fixtures/offset_anonymous_struct.golden: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func test(arg struct { 4 | Field int `json:"field"` 5 | }) { 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/offset_anonymous_struct.input: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func test(arg struct { 4 | Field int 5 | }) { 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/offset_array_struct.golden: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var x []struct { 4 | Field int `json:"field"` 5 | } 6 | -------------------------------------------------------------------------------- /test-fixtures/offset_array_struct.input: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var x []struct { 4 | Field int 5 | } -------------------------------------------------------------------------------- /test-fixtures/offset_star_struct.golden: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var x *struct { 4 | Field int `json:"field"` 5 | } 6 | -------------------------------------------------------------------------------- /test-fixtures/offset_star_struct.input: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var x *struct { 4 | Field int 5 | } -------------------------------------------------------------------------------- /test-fixtures/skip_embedded.golden: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import "time" 4 | 5 | type StationEvent interface { 6 | DomainEvent 7 | isStationEvent() 8 | } 9 | 10 | type stationEvent struct{} 11 | 12 | func (evt stationEvent) isStationEvent() {} 13 | 14 | type StationCreated struct { 15 | stationEvent 16 | ConnectorID int `json:"connector_id"` 17 | MeterStop int `json:"meter_stop"` 18 | TransactionID int32 `json:"transaction_id"` 19 | ChargingCardID identifier.ChargingCardID `json:"charging_card_id"` 20 | Timestamp time.Time `json:"timestamp` 21 | } 22 | -------------------------------------------------------------------------------- /test-fixtures/skip_embedded.input: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import "time" 4 | 5 | type StationEvent interface { 6 | DomainEvent 7 | isStationEvent() 8 | } 9 | 10 | type stationEvent struct{} 11 | 12 | func (evt stationEvent) isStationEvent() {} 13 | 14 | type StationCreated struct { 15 | stationEvent 16 | ConnectorID int 17 | MeterStop int 18 | TransactionID int32 19 | ChargingCardID identifier.ChargingCardID 20 | Timestamp time.Time `json:"timestamp` 21 | } 22 | -------------------------------------------------------------------------------- /test-fixtures/skip_private.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | Bar string `json:"bar"` 5 | t bool 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/skip_private.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | Bar string 5 | t bool 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/skip_private_multiple_names.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | Bar string `json:"bar"` 5 | t, Baz bool `json:"baz"` 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/skip_private_multiple_names.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | Bar string 5 | t, Baz bool 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/struct_add.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar"` 5 | t bool `json:"t"` 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/struct_add.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | t bool 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/struct_add_existing.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar"` 5 | timestamp time.Time `json:"@timestamp"` 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/struct_add_existing.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | timestamp time.Time `json:"@timestamp"` 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/struct_add_underscore.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | foo_bar string `json:"foo_bar"` 5 | } 6 | -------------------------------------------------------------------------------- /test-fixtures/struct_add_underscore.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | foo_bar string 5 | } 6 | -------------------------------------------------------------------------------- /test-fixtures/struct_clear_options.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar" hcl:"bar"` 5 | t bool `hcl:"t"` 6 | t bool `hcl:"t"` 7 | qux string `json:"qux" hcl:"qux"` 8 | yoo string `json:"yoo"` 9 | } 10 | -------------------------------------------------------------------------------- /test-fixtures/struct_clear_options.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar,omitempty" hcl:"bar,omitnested"` 5 | t bool `hcl:"t"` 6 | t bool `hcl:"t,omitempty"` 7 | qux string `json:"qux,omitempty" hcl:"qux,squash,keys"` 8 | yoo string `json:"yoo"` 9 | } 10 | -------------------------------------------------------------------------------- /test-fixtures/struct_clear_tags.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | t bool 6 | t bool 7 | qux string 8 | yoo string 9 | } 10 | -------------------------------------------------------------------------------- /test-fixtures/struct_clear_tags.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar,omitempty" hcl:"bar,omitnested"` 5 | t bool `hcl:"t"` 6 | t bool `hcl:"t,omitempty"` 7 | qux string `json:"qux,omitempty" hcl:"qux,squash,keys"` 8 | yoo string `json:"yoo"` 9 | } 10 | -------------------------------------------------------------------------------- /test-fixtures/struct_format.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `gaum:"field_name=bar"` 5 | t bool `gaum:"field_name=t"` 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/struct_format.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | t bool 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/struct_format_existing.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `gaum:"field_name=bar"` 5 | timestamp time.Time `gaum:"@timestamp"` 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/struct_format_existing.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | timestamp time.Time `gaum:"@timestamp"` 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/struct_format_existing_oldstyle.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `gaum:"field_name=bar"` 5 | timestamp time.Time `gaum:"@timestamp"` 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/struct_format_existing_oldstyle.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | timestamp time.Time `gaum:"@timestamp"` 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/struct_format_oldstyle.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `gaum:"field_name=bar"` 5 | t bool `gaum:"field_name=t"` 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/struct_format_oldstyle.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | t bool 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/struct_remove.golden: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string 5 | t bool `hcl:"t"` 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/struct_remove.input: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | type foo struct { 4 | bar string `json:"bar"` 5 | t bool `hcl:"t"` 6 | } 7 | --------------------------------------------------------------------------------