diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 0df9133..414c525 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -1,25 +1,50 @@ name: build -on: [push, pull_request] -jobs: +on: + push: + branches: + - main + pull_request: + +jobs: test-build: name: Test & Build runs-on: ubuntu-latest steps: - - name: Set up Go 1.15 - uses: actions/setup-go@v1 + - name: Set up Go + uses: actions/setup-go@v2 with: - go-version: 1.15 - id: go + go-version: ^1.17 - name: Check out code into the Go module directory - uses: actions/checkout@v1 + uses: actions/checkout@v2 + + - name: Run go mod tidy + run: | + set -e + go mod tidy + output=$(git status -s) + if [ -z "${output}" ]; then + exit 0 + fi + echo 'We wish to maintain a tidy state for go mod. Please run `go mod tidy` on your branch, commit and push again.' + echo 'Running `go mod tidy` on this CI test yields with the following changes:' + echo "$output" + exit 1 - name: Test run: | - go mod tidy -v go test -race ./... + - name: Go vet + run: "go vet ./..." + + - name: Staticcheck + uses: dominikh/staticcheck-action@v1.1.0 + with: + version: "2021.1.1" + install-go: false + - name: Build run: go build ./... diff --git a/internal/gomodifytags/gomodifytags.go b/internal/gomodifytags/gomodifytags.go new file mode 100644 index 0000000..a174323 --- /dev/null +++ b/internal/gomodifytags/gomodifytags.go @@ -0,0 +1,801 @@ +package gomodifytags + +import ( + "bufio" + "bytes" + "encoding/json" + "errors" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/printer" + "go/token" + "io" + "io/ioutil" + "sort" + "strconv" + "strings" + "unicode" + + "github.com/fatih/camelcase" + "github.com/fatih/structtag" + "golang.org/x/tools/go/buildutil" +) + +// structType contains a structType node and it's name. It's a convenient +// helper type, because *ast.StructType doesn't contain the name of the struct +type structType struct { + name string + node *ast.StructType +} + +// output is used usually by editors +type output struct { + Start int `json:"start"` + End int `json:"end"` + Lines []string `json:"lines"` + Errors []string `json:"errors,omitempty"` +} + +// Config configures gomodify tags and describes how tags should be modified. +type Config struct { + fset *token.FileSet + + File string + Output string + Quiet bool + Write bool + Modified io.Reader + + Offset int + StructName string + FieldName string + Line string + Start, End int + All bool + + Remove []string + RemoveOptions []string + + Add []string + AddOptions []string + Override bool + SkipUnexportedFields bool + + Transform string + Sort bool + ValueFormat string + Clear bool + ClearOption bool +} + +// Run runs gomodofiytags with the config. +func (c *Config) Run() error { + err := c.validate() + if err != nil { + return err + } + + node, err := c.parse() + if err != nil { + return err + } + + start, end, err := c.findSelection(node) + if err != nil { + return err + } + + rewrittenNode, errs := c.rewrite(node, start, end) + if errs != nil { + if _, ok := errs.(*rewriteErrors); !ok { + return errs + } + } + + out, err := c.format(rewrittenNode, errs) + if err != nil { + return err + } + + if !c.Quiet { + fmt.Println(out) + } + + return nil +} + +func (c *Config) parse() (ast.Node, error) { + c.fset = token.NewFileSet() + var contents interface{} + if c.Modified != nil { + archive, err := buildutil.ParseOverlayArchive(c.Modified) + if err != nil { + return nil, fmt.Errorf("failed to parse -modified archive: %v", err) + } + fc, ok := archive[c.File] + if !ok { + return nil, fmt.Errorf("couldn't find %s in archive", c.File) + } + contents = fc + } + + return parser.ParseFile(c.fset, c.File, contents, parser.ParseComments) +} + +// findSelection returns the start and end position of the fields that are +// suspect to change. It depends on the line, struct or offset selection. +func (c *Config) findSelection(node ast.Node) (int, int, error) { + if c.Line != "" { + return c.lineSelection(node) + } else if c.Offset != 0 { + return c.offsetSelection(node) + } else if c.StructName != "" { + return c.structSelection(node) + } else if c.All { + return c.allSelection(node) + } else { + return 0, 0, errors.New("-line, -offset, -struct or -all is not passed") + } +} + +func (c *Config) process(fieldName, tagVal string) (string, error) { + var tag string + if tagVal != "" { + var err error + tag, err = strconv.Unquote(tagVal) + if err != nil { + return "", err + } + } + + tags, err := structtag.Parse(tag) + if err != nil { + return "", err + } + + tags = c.removeTags(tags) + tags, err = c.removeTagOptions(tags) + if err != nil { + return "", err + } + + tags = c.clearTags(tags) + tags = c.clearOptions(tags) + + tags, err = c.addTags(fieldName, tags) + if err != nil { + return "", err + } + + tags, err = c.addTagOptions(tags) + if err != nil { + return "", err + } + + if c.Sort { + sort.Sort(tags) + } + + res := tags.String() + if res != "" { + res = quote(tags.String()) + } + + return res, nil +} + +func (c *Config) removeTags(tags *structtag.Tags) *structtag.Tags { + if c.Remove == nil || len(c.Remove) == 0 { + return tags + } + + tags.Delete(c.Remove...) + return tags +} + +func (c *Config) clearTags(tags *structtag.Tags) *structtag.Tags { + if !c.Clear { + return tags + } + + tags.Delete(tags.Keys()...) + return tags +} + +func (c *Config) clearOptions(tags *structtag.Tags) *structtag.Tags { + if !c.ClearOption { + return tags + } + + for _, t := range tags.Tags() { + t.Options = nil + } + + return tags +} + +func (c *Config) removeTagOptions(tags *structtag.Tags) (*structtag.Tags, error) { + if c.RemoveOptions == nil || len(c.RemoveOptions) == 0 { + return tags, nil + } + + for _, val := range c.RemoveOptions { + // syntax key=option + splitted := strings.Split(val, "=") + if len(splitted) < 2 { + return nil, errors.New("wrong syntax to remove an option. i.e key=option") + } + + key := splitted[0] + option := strings.Join(splitted[1:], "=") + + tags.DeleteOptions(key, option) + } + + return tags, nil +} + +func (c *Config) addTagOptions(tags *structtag.Tags) (*structtag.Tags, error) { + if c.AddOptions == nil || len(c.AddOptions) == 0 { + return tags, nil + } + + for _, val := range c.AddOptions { + // syntax key=option + splitted := strings.Split(val, "=") + if len(splitted) < 2 { + return nil, errors.New("wrong syntax to add an option. i.e key=option") + } + + key := splitted[0] + option := strings.Join(splitted[1:], "=") + + tags.AddOptions(key, option) + } + + return tags, nil +} + +func (c *Config) addTags(fieldName string, tags *structtag.Tags) (*structtag.Tags, error) { + if c.Add == nil || len(c.Add) == 0 { + return tags, nil + } + + splitted := camelcase.Split(fieldName) + name := "" + + unknown := false + switch c.Transform { + case "snakecase": + var lowerSplitted []string + for _, s := range splitted { + lowerSplitted = append(lowerSplitted, strings.ToLower(s)) + } + + name = strings.Join(lowerSplitted, "_") + case "lispcase": + var lowerSplitted []string + for _, s := range splitted { + lowerSplitted = append(lowerSplitted, strings.ToLower(s)) + } + + name = strings.Join(lowerSplitted, "-") + case "camelcase": + var titled []string + for _, s := range splitted { + titled = append(titled, strings.Title(s)) + } + + titled[0] = strings.ToLower(titled[0]) + + name = strings.Join(titled, "") + case "pascalcase": + var titled []string + for _, s := range splitted { + titled = append(titled, strings.Title(s)) + } + + name = strings.Join(titled, "") + case "titlecase": + var titled []string + for _, s := range splitted { + titled = append(titled, strings.Title(s)) + } + + name = strings.Join(titled, " ") + case "keep": + name = fieldName + default: + unknown = true + } + + if c.ValueFormat != "" { + prevName := name + name = strings.ReplaceAll(c.ValueFormat, "{field}", name) + if name == c.ValueFormat { + // support old style for backward compatibility + name = strings.ReplaceAll(c.ValueFormat, "$field", prevName) + } + } + + for _, key := range c.Add { + splitted = strings.SplitN(key, ":", 2) + if len(splitted) >= 2 { + key = splitted[0] + name = strings.Join(splitted[1:], "") + } else if unknown { + // the user didn't pass any value but want to use an unknown + // transform. We don't return above in the default as the user + // might pass a value + return nil, fmt.Errorf("unknown transform option %q", c.Transform) + } + + tag, err := tags.Get(key) + if err != nil { + // tag doesn't exist, create a new one + tag = &structtag.Tag{ + Key: key, + Name: name, + } + } else if c.Override { + tag.Name = name + } + + if err := tags.Set(tag); err != nil { + return nil, err + } + } + + return tags, nil +} + +// collectStructs collects and maps structType nodes to their positions +func collectStructs(node ast.Node) map[token.Pos]*structType { + structs := make(map[token.Pos]*structType) + + collectStructs := func(n ast.Node) bool { + var t ast.Expr + var structName string + + switch x := n.(type) { + case *ast.TypeSpec: + if x.Type == nil { + return true + + } + + structName = x.Name.Name + t = x.Type + case *ast.CompositeLit: + t = x.Type + case *ast.ValueSpec: + structName = x.Names[0].Name + t = x.Type + case *ast.Field: + // this case also catches struct fields and the structName + // therefore might contain the field name (which is wrong) + // because `x.Type` in this case is not a *ast.StructType. + // + // We're OK with it, because, in our case *ast.Field represents + // a parameter declaration, i.e: + // + // func test(arg struct { + // Field int + // }) { + // } + // + // and hence the struct name will be `arg`. + if len(x.Names) != 0 { + structName = x.Names[0].Name + } + t = x.Type + } + + // if expression is in form "*T" or "[]T", dereference to check if "T" + // contains a struct expression + t = deref(t) + + x, ok := t.(*ast.StructType) + if !ok { + return true + } + + structs[x.Pos()] = &structType{ + name: structName, + node: x, + } + return true + } + + ast.Inspect(node, collectStructs) + return structs +} + +func (c *Config) format(file ast.Node, rwErrs error) (string, error) { + switch c.Output { + case "source": + var buf bytes.Buffer + err := format.Node(&buf, c.fset, file) + if err != nil { + return "", err + } + + if c.Write { + err = ioutil.WriteFile(c.File, buf.Bytes(), 0) + if err != nil { + return "", err + } + } + + return buf.String(), nil + case "json": + // NOTE(arslan): print first the whole file and then cut out our + // selection. The reason we don't directly print the struct is that the + // printer is not capable of printing loosy comments, comments that are + // not part of any field inside a struct. Those are part of *ast.File + // and only printed inside a struct if we print the whole file. This + // approach is the sanest and simplest way to get a struct printed + // back. Second, our cursor might intersect two different structs with + // other declarations in between them. Printing the file and cutting + // the selection is the easier and simpler to do. + var buf bytes.Buffer + + // this is the default config from `format.Node()`, but we add + // `printer.SourcePos` to get the original source position of the + // modified lines + cfg := printer.Config{Mode: printer.SourcePos | printer.UseSpaces | printer.TabIndent, Tabwidth: 8} + err := cfg.Fprint(&buf, c.fset, file) + if err != nil { + return "", err + } + + lines, err := parseLines(&buf) + if err != nil { + return "", err + } + + // prevent selection to be larger than the actual number of lines + if c.Start > len(lines) || c.End > len(lines) { + return "", errors.New("line selection is invalid") + } + + out := &output{ + Start: c.Start, + End: c.End, + Lines: lines[c.Start-1 : c.End], + } + + if rwErrs != nil { + if r, ok := rwErrs.(*rewriteErrors); ok { + for _, err := range r.errs { + out.Errors = append(out.Errors, err.Error()) + } + } + } + + o, err := json.MarshalIndent(out, "", " ") + if err != nil { + return "", err + } + + return string(o), nil + default: + return "", fmt.Errorf("unknown output mode: %s", c.Output) + } +} + +func (c *Config) lineSelection(file ast.Node) (int, int, error) { + var err error + splitted := strings.Split(c.Line, ",") + + start, err := strconv.Atoi(splitted[0]) + if err != nil { + return 0, 0, err + } + + end := start + if len(splitted) == 2 { + end, err = strconv.Atoi(splitted[1]) + if err != nil { + return 0, 0, err + } + } + + if start > end { + return 0, 0, errors.New("wrong range. start line cannot be larger than end line") + } + + return start, end, nil +} + +func (c *Config) structSelection(file ast.Node) (int, int, error) { + structs := collectStructs(file) + + var encStruct *ast.StructType + for _, st := range structs { + if st.name == c.StructName { + encStruct = st.node + } + } + + if encStruct == nil { + return 0, 0, errors.New("struct name does not exist") + } + + // if field name has been specified as well, only select the given field + if c.FieldName != "" { + return c.fieldSelection(encStruct) + } + + start := c.fset.Position(encStruct.Pos()).Line + end := c.fset.Position(encStruct.End()).Line + + return start, end, nil +} + +func (c *Config) fieldSelection(st *ast.StructType) (int, int, error) { + var encField *ast.Field + for _, f := range st.Fields.List { + for _, field := range f.Names { + if field.Name == c.FieldName { + encField = f + } + } + } + + if encField == nil { + return 0, 0, fmt.Errorf("struct %q doesn't have field name %q", c.StructName, c.FieldName) + } + + start := c.fset.Position(encField.Pos()).Line + end := c.fset.Position(encField.End()).Line + + return start, end, nil +} + +func (c *Config) offsetSelection(file ast.Node) (int, int, error) { + structs := collectStructs(file) + + var encStruct *ast.StructType + for _, st := range structs { + structBegin := c.fset.Position(st.node.Pos()).Offset + structEnd := c.fset.Position(st.node.End()).Offset + + if structBegin <= c.Offset && c.Offset <= structEnd { + encStruct = st.node + break + } + } + + if encStruct == nil { + return 0, 0, errors.New("offset is not inside a struct") + } + + // offset selects all fields + start := c.fset.Position(encStruct.Pos()).Line + end := c.fset.Position(encStruct.End()).Line + + return start, end, nil +} + +// allSelection selects all structs inside a file +func (c *Config) allSelection(file ast.Node) (int, int, error) { + start := 1 + end := c.fset.File(file.Pos()).LineCount() + + return start, end, nil +} + +func isPublicName(name string) bool { + for _, c := range name { + return unicode.IsUpper(c) + } + return false +} + +// rewrite rewrites the node for structs between the start and end +// positions +func (c *Config) rewrite(node ast.Node, start, end int) (ast.Node, error) { + errs := &rewriteErrors{errs: make([]error, 0)} + + rewriteFunc := func(n ast.Node) bool { + x, ok := n.(*ast.StructType) + if !ok { + return true + } + + for _, f := range x.Fields.List { + line := c.fset.Position(f.Pos()).Line + + if !(start <= line && line <= end) { + continue + } + + fieldName := "" + if len(f.Names) != 0 { + for _, field := range f.Names { + if !c.SkipUnexportedFields || isPublicName(field.Name) { + fieldName = field.Name + break + } + } + } + + // anonymous field + if f.Names == nil { + ident, ok := f.Type.(*ast.Ident) + if !ok { + continue + } + + if !c.SkipUnexportedFields { + fieldName = ident.Name + } + } + + // nothing to process, continue with next line + if fieldName == "" { + continue + } + + if f.Tag == nil { + f.Tag = &ast.BasicLit{} + } + + res, err := c.process(fieldName, f.Tag.Value) + if err != nil { + errs.Append(fmt.Errorf("%s:%d:%d:%s", + c.fset.Position(f.Pos()).Filename, + c.fset.Position(f.Pos()).Line, + c.fset.Position(f.Pos()).Column, + err)) + continue + } + + f.Tag.Value = res + } + + return true + } + + ast.Inspect(node, rewriteFunc) + + c.Start = start + c.End = end + + if len(errs.errs) == 0 { + return node, nil + } + + return node, errs +} + +// validate validates whether the config is valid or not +func (c *Config) validate() error { + if c.File == "" { + return errors.New("no file is passed") + } + + if c.Line == "" && c.Offset == 0 && c.StructName == "" && !c.All { + return errors.New("-line, -offset, -struct or -all is not passed") + } + + if c.Line != "" && c.Offset != 0 || + c.Line != "" && c.StructName != "" || + c.Offset != 0 && c.StructName != "" { + return errors.New("-line, -offset or -struct cannot be used together. pick one") + } + + if (c.Add == nil || len(c.Add) == 0) && + (c.AddOptions == nil || len(c.AddOptions) == 0) && + !c.Clear && + !c.ClearOption && + (c.RemoveOptions == nil || len(c.RemoveOptions) == 0) && + (c.Remove == nil || len(c.Remove) == 0) { + return errors.New("one of " + + "[-add-tags, -add-options, -remove-tags, -remove-options, -clear-tags, -clear-options]" + + " should be defined") + } + + if c.FieldName != "" && c.StructName == "" { + return errors.New("-field is requiring -struct") + } + + return nil +} + +func quote(tag string) string { + return "`" + tag + "`" +} + +type rewriteErrors struct { + errs []error +} + +func (r *rewriteErrors) Error() string { + var buf bytes.Buffer + for _, e := range r.errs { + buf.WriteString(fmt.Sprintf("%s\n", e.Error())) + } + return buf.String() +} + +func (r *rewriteErrors) Append(err error) { + if err == nil { + return + } + + r.errs = append(r.errs, err) +} + +// parseLines parses the given buffer and returns a slice of lines +func parseLines(buf io.Reader) ([]string, error) { + var lines []string + scanner := bufio.NewScanner(buf) + for scanner.Scan() { + txt := scanner.Text() + + // check for any line directive and store it for next iteration to + // re-construct the original file. If it's not a line directive, + // continue consturcting the original file + if !strings.HasPrefix(txt, "//line") { + lines = append(lines, txt) + continue + } + + lineNr, err := split(txt) + if err != nil { + return nil, err + } + + for i := len(lines); i < lineNr-1; i++ { + lines = append(lines, "") + } + + lines = lines[:lineNr-1] + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("invalid scanner inputl: %s", err) + } + + return lines, nil +} + +// split splits the given line directive and returns the line number +// see https://golang.org/cmd/compile/#hdr-Compiler_Directives for more +// information +// NOTE(arslan): this only splits the line directive that the go.Parser +// outputs. If the go parser changes the format of the line directive, make +// sure to fix it in the below function +func split(line string) (int, error) { + for i := len(line) - 1; i >= 0; i-- { + if line[i] != ':' { + continue + } + + nr, err := strconv.Atoi(line[i+1:]) + if err != nil { + return 0, err + } + + return nr, nil + } + + return 0, fmt.Errorf("couldn't parse line: '%s'", line) +} + +// deref takes an expression, and removes all its leading "*" and "[]" +// operator. Uuse case : if found expression is a "*t" or "[]t", we need to +// check if "t" contains a struct expression. +func deref(x ast.Expr) ast.Expr { + switch t := x.(type) { + case *ast.StarExpr: + return deref(t.X) + case *ast.ArrayType: + return deref(t.Elt) + } + return x +} diff --git a/internal/gomodifytags/gomodifytags_test.go b/internal/gomodifytags/gomodifytags_test.go new file mode 100644 index 0000000..000bce1 --- /dev/null +++ b/internal/gomodifytags/gomodifytags_test.go @@ -0,0 +1,929 @@ +package gomodifytags + +import ( + "bytes" + "errors" + "flag" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "strings" + "testing" +) + +var update = flag.Bool("update", false, "update golden (.out) files") + +// This is the directory where our test fixtures are. +const fixtureDir = "./test-fixtures" + +func TestRewrite(t *testing.T) { + test := []struct { + cfg *Config + file string + }{ + { + file: "struct_add", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + StructName: "foo", + Transform: "snakecase", + }, + }, + { + file: "struct_add_existing", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + StructName: "foo", + Transform: "snakecase", + }, + }, + { + file: "struct_format", + cfg: &Config{ + Add: []string{"gaum"}, + Output: "source", + StructName: "foo", + Transform: "snakecase", + ValueFormat: "field_name={field}", + }, + }, + { + file: "struct_format_existing", + cfg: &Config{ + Add: []string{"gaum"}, + Output: "source", + StructName: "foo", + Transform: "snakecase", + ValueFormat: "field_name={field}", + }, + }, + { + file: "struct_format_oldstyle", + cfg: &Config{ + Add: []string{"gaum"}, + Output: "source", + StructName: "foo", + Transform: "snakecase", + ValueFormat: "field_name=$field", + }, + }, + { + file: "struct_format_existing_oldstyle", + cfg: &Config{ + Add: []string{"gaum"}, + Output: "source", + StructName: "foo", + Transform: "snakecase", + ValueFormat: "field_name=$field", + }, + }, + { + file: "struct_remove", + cfg: &Config{ + Remove: []string{"json"}, + Output: "source", + StructName: "foo", + }, + }, + { + file: "struct_clear_tags", + cfg: &Config{ + Clear: true, + Output: "source", + StructName: "foo", + }, + }, + { + file: "struct_clear_options", + cfg: &Config{ + ClearOption: true, + Output: "source", + StructName: "foo", + }, + }, + { + file: "line_add", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + Line: "4", + Transform: "snakecase", + }, + }, + { + file: "line_add_override", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + Line: "4,5", + Transform: "snakecase", + Override: true, + }, + }, + { + file: "line_add_override_column", + cfg: &Config{ + Add: []string{"json:MyBar:bar"}, + Output: "source", + Line: "4,4", + Transform: "snakecase", + Override: true, + }, + }, + { + file: "line_add_override_mixed_column_and_equal", + cfg: &Config{ + Add: []string{"json:MyBar:bar:foo=qux"}, + Output: "source", + Line: "4,4", + Transform: "snakecase", + Override: true, + }, + }, + { + file: "line_add_override_multi_equal", + cfg: &Config{ + Add: []string{"json:MyBar=bar=foo"}, + Output: "source", + Line: "4,4", + Transform: "snakecase", + Override: true, + }, + }, + { + file: "line_add_override_multi_column", + cfg: &Config{ + Add: []string{"json:MyBar:bar:foo"}, + Output: "source", + Line: "4,4", + Transform: "snakecase", + Override: true, + }, + }, + { + file: "line_add_no_override", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + Line: "4,5", + Transform: "snakecase", + }, + }, + { + file: "line_add_outside", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + Line: "2,8", + Transform: "snakecase", + }, + }, + { + file: "line_add_outside_partial_start", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + Line: "2,5", + Transform: "snakecase", + }, + }, + { + file: "line_add_outside_partial_end", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + Line: "5,8", + Transform: "snakecase", + }, + }, + { + file: "line_add_intersect_partial", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + Line: "5,11", + Transform: "snakecase", + }, + }, + { + file: "line_add_comment", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + Line: "6,7", + Transform: "snakecase", + }, + }, + { + file: "line_add_option", + cfg: &Config{ + AddOptions: []string{"json=omitempty"}, + Output: "source", + Line: "4,7", + }, + }, + { + file: "line_add_option_existing", + cfg: &Config{ + AddOptions: []string{"json=omitempty"}, + Output: "source", + Line: "6,8", + }, + }, + { + file: "line_add_multiple_option", + cfg: &Config{ + AddOptions: []string{"json=omitempty", "hcl=squash"}, + Add: []string{"hcl"}, + Output: "source", + Line: "4,7", + Transform: "snakecase", + }, + }, + { + file: "line_add_option_with_equal", + cfg: &Config{ + AddOptions: []string{"validate=max=32"}, + Add: []string{"validate"}, + Output: "source", + Line: "4,7", + Transform: "snakecase", + }, + }, + { + file: "line_remove", + cfg: &Config{ + Remove: []string{"json"}, + Output: "source", + Line: "5,7", + }, + }, + { + file: "line_remove_option", + cfg: &Config{ + RemoveOptions: []string{"hcl=squash"}, + Output: "source", + Line: "4,8", + }, + }, + { + file: "line_remove_options", + cfg: &Config{ + RemoveOptions: []string{"json=omitempty", "hcl=omitnested"}, + Output: "source", + Line: "4,7", + }, + }, + { + file: "line_remove_option_with_equal", + cfg: &Config{ + RemoveOptions: []string{"validate=max=32"}, + Output: "source", + Line: "4,7", + }, + }, + { + file: "line_multiple_add", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + Line: "5,6", + Transform: "camelcase", + }, + }, + { + file: "line_lispcase_add", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + Line: "4,6", + Transform: "lispcase", + }, + }, + { + file: "line_camelcase_add", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + Line: "4,5", + Transform: "camelcase", + }, + }, + { + file: "line_camelcase_add_embedded", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + Line: "4,6", + Transform: "camelcase", + }, + }, + { + file: "line_value_add", + cfg: &Config{ + Add: []string{"json:foo"}, + Output: "source", + Line: "4,6", + }, + }, + { + file: "offset_add", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + Offset: 32, + Transform: "snakecase", + }, + }, + { + file: "offset_add_composite", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + Offset: 40, + Transform: "snakecase", + }, + }, + { + file: "offset_add_duplicate", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + Offset: 209, + Transform: "snakecase", + }, + }, + { + file: "offset_add_literal_in", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + Offset: 46, + Transform: "snakecase", + }, + }, + { + file: "offset_add_literal_out", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + Offset: 32, + Transform: "snakecase", + }, + }, + { + file: "errors", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + Line: "4,7", + Transform: "snakecase", + }, + }, + { + file: "line_pascalcase_add", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + Line: "4,5", + Transform: "pascalcase", + }, + }, + { + file: "line_pascalcase_add_embedded", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + Line: "4,6", + Transform: "pascalcase", + }, + }, + { + file: "not_formatted", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + Line: "3,4", + Transform: "snakecase", + }, + }, + { + file: "skip_private", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + StructName: "foo", + Transform: "snakecase", + SkipUnexportedFields: true, + }, + }, + { + file: "skip_private_multiple_names", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + StructName: "foo", + Transform: "snakecase", + SkipUnexportedFields: true, + }, + }, + { + file: "skip_embedded", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + StructName: "StationCreated", + Transform: "snakecase", + SkipUnexportedFields: true, + }, + }, + { + file: "all_structs", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + All: true, + Transform: "snakecase", + }, + }, + { + file: "line_titlecase_add", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + Line: "4,6", + Transform: "titlecase", + }, + }, + { + file: "line_titlecase_add_embedded", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + Line: "4,6", + Transform: "titlecase", + }, + }, + { + file: "field_add", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + StructName: "foo", + FieldName: "bar", + Transform: "snakecase", + }, + }, + { + file: "field_add_same_line", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + StructName: "foo", + FieldName: "qux", + Transform: "snakecase", + }, + }, + { + file: "field_add_existing", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + StructName: "foo", + FieldName: "bar", + Transform: "snakecase", + }, + }, + { + file: "field_clear_tags", + cfg: &Config{ + Clear: true, + Output: "source", + StructName: "foo", + FieldName: "bar", + }, + }, + { + file: "field_clear_options", + cfg: &Config{ + ClearOption: true, + Output: "source", + StructName: "foo", + FieldName: "bar", + }, + }, + { + file: "field_remove", + cfg: &Config{ + Remove: []string{"json"}, + Output: "source", + StructName: "foo", + FieldName: "bar", + }, + }, + { + file: "offset_anonymous_struct", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + Offset: 45, + Transform: "camelcase", + }, + }, + { + file: "offset_star_struct", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + Offset: 35, + Transform: "camelcase", + }, + }, + { + file: "offset_array_struct", + cfg: &Config{ + Add: []string{"json"}, + Output: "source", + Offset: 35, + Transform: "camelcase", + }, + }, + } + + for _, ts := range test { + t.Run(ts.file, func(t *testing.T) { + ts.cfg.File = filepath.Join(fixtureDir, fmt.Sprintf("%s.input", ts.file)) + + node, err := ts.cfg.parse() + if err != nil { + t.Fatal(err) + } + + start, end, err := ts.cfg.findSelection(node) + if err != nil { + t.Fatal(err) + } + + rewrittenNode, err := ts.cfg.rewrite(node, start, end) + if err != nil { + if _, ok := err.(*rewriteErrors); !ok { + t.Fatal(err) + } + } + + out, err := ts.cfg.format(rewrittenNode, err) + if err != nil { + t.Fatal(err) + } + got := []byte(out) + + // update golden file if necessary + golden := filepath.Join(fixtureDir, fmt.Sprintf("%s.golden", ts.file)) + if *update { + err := ioutil.WriteFile(golden, got, 0644) + if err != nil { + t.Error(err) + } + return + } + + // get golden file + want, err := ioutil.ReadFile(golden) + if err != nil { + t.Fatal(err) + } + + var from []byte + if ts.cfg.Modified != nil { + from, err = ioutil.ReadAll(ts.cfg.Modified) + } else { + from, err = ioutil.ReadFile(ts.cfg.File) + } + if err != nil { + t.Fatal(err) + } + + // compare + if !bytes.Equal(got, want) { + t.Errorf("case %s\ngot:\n====\n\n%s\nwant:\n=====\n\n%s\nfrom:\n=====\n\n%s\n", + ts.file, got, want, from) + } + }) + } +} + +func TestJSON(t *testing.T) { + test := []struct { + cfg *Config + file string + err error + }{ + { + file: "json_single", + cfg: &Config{ + Add: []string{"json"}, + Line: "5", + }, + }, + { + file: "json_full", + cfg: &Config{ + Add: []string{"json"}, + Line: "4,6", + }, + }, + { + file: "json_intersection", + cfg: &Config{ + Add: []string{"json"}, + Line: "5,16", + }, + }, + { + // both small & end range larger than file + file: "json_single", + cfg: &Config{ + Add: []string{"json"}, + Line: "30,32", // invalid selection + }, + err: errors.New("line selection is invalid"), + }, + { + // end range larger than file + file: "json_single", + cfg: &Config{ + Add: []string{"json"}, + Line: "4,50", // invalid selection + }, + err: errors.New("line selection is invalid"), + }, + { + file: "json_errors", + cfg: &Config{ + Add: []string{"json"}, + Line: "4,7", + }, + }, + { + file: "json_not_formatted", + cfg: &Config{ + Add: []string{"json"}, + Line: "3,4", + }, + }, + { + file: "json_not_formatted_2", + cfg: &Config{ + Add: []string{"json"}, + Line: "3,3", + }, + }, + { + file: "json_not_formatted_3", + cfg: &Config{ + Add: []string{"json"}, + Offset: 23, + }, + }, + { + file: "json_not_formatted_4", + cfg: &Config{ + Add: []string{"json"}, + Offset: 51, + }, + }, + { + file: "json_not_formatted_5", + cfg: &Config{ + Add: []string{"json"}, + Offset: 29, + }, + }, + { + file: "json_not_formatted_6", + cfg: &Config{ + Add: []string{"json"}, + Line: "2,54", + }, + }, + { + file: "json_all_structs", + cfg: &Config{ + Add: []string{"json"}, + All: true, + }, + }, + } + + for _, ts := range test { + t.Run(ts.file, func(t *testing.T) { + ts.cfg.File = filepath.Join(fixtureDir, fmt.Sprintf("%s.input", ts.file)) + // these are explicit and shouldn't be changed for this particular + // main test + ts.cfg.Output = "json" + ts.cfg.Transform = "camelcase" + + node, err := ts.cfg.parse() + if err != nil { + t.Fatal(err) + } + + start, end, err := ts.cfg.findSelection(node) + if err != nil { + t.Fatal(err) + } + + rewrittenNode, err := ts.cfg.rewrite(node, start, end) + if err != nil { + if _, ok := err.(*rewriteErrors); !ok { + t.Fatal(err) + } + } + + out, err := ts.cfg.format(rewrittenNode, err) + if !reflect.DeepEqual(err, ts.err) { + t.Logf("want: %v", ts.err) + t.Logf("got: %v", err) + t.Fatalf("unexpected error") + } + + if ts.err != nil { + return + } + + got := []byte(out) + + // update golden file if necessary + golden := filepath.Join(fixtureDir, fmt.Sprintf("%s.golden", ts.file)) + if *update { + err := ioutil.WriteFile(golden, got, 0644) + if err != nil { + t.Error(err) + } + return + } + + // get golden file + want, err := ioutil.ReadFile(golden) + if err != nil { + t.Fatal(err) + } + + from, err := ioutil.ReadFile(ts.cfg.File) + if err != nil { + t.Fatal(err) + } + + // compare + if !bytes.Equal(got, want) { + t.Errorf("case %s\ngot:\n====\n\n%s\nwant:\n=====\n\n%s\nfrom:\n=====\n\n%s\n", + ts.file, got, want, from) + } + }) + } +} + +func TestModifiedRewrite(t *testing.T) { + cfg := &Config{ + Add: []string{"json"}, + Output: "source", + StructName: "foo", + Transform: "snakecase", + File: "struct_add_modified", + Modified: strings.NewReader(`struct_add_modified +55 +package foo + +type foo struct { + bar string + t bool +} +`), + } + + node, err := cfg.parse() + if err != nil { + t.Fatal(err) + } + + start, end, err := cfg.findSelection(node) + if err != nil { + t.Fatal(err) + } + + rewrittenNode, err := cfg.rewrite(node, start, end) + if err != nil { + t.Fatal(err) + } + + got, err := cfg.format(rewrittenNode, err) + if err != nil { + t.Fatal(err) + } + + golden := filepath.Join(fixtureDir, "struct_add.golden") + want, err := ioutil.ReadFile(golden) + if err != nil { + t.Fatal(err) + } + + // compare + if !bytes.Equal([]byte(got), want) { + t.Errorf("got:\n====\n%s\nwant:\n====\n%s\n", got, want) + } +} + +func TestModifiedFileMissing(t *testing.T) { + cfg := &Config{ + Add: []string{"json"}, + Output: "source", + StructName: "foo", + Transform: "snakecase", + File: "struct_add_modified", + Modified: strings.NewReader(`file_that_doesnt_exist +55 +package foo + +type foo struct { + bar string + t bool +} +`), + } + + _, err := cfg.parse() + if err == nil { + t.Fatal("expected error") + } +} + +func TestParseLines(t *testing.T) { + var tests = []struct { + file string + }{ + {file: "line_directive_unix"}, + {file: "line_directive_windows"}, + } + + for _, ts := range tests { + ts := ts + + t.Run(ts.file, func(t *testing.T) { + filePath := filepath.Join(fixtureDir, fmt.Sprintf("%s.input", ts.file)) + + file, err := os.Open(filePath) + if err != nil { + t.Fatal(err) + } + defer file.Close() + + out, err := parseLines(file) + if err != nil { + t.Fatal(err) + } + + toBytes := func(lines []string) []byte { + var buf bytes.Buffer + for _, line := range lines { + buf.WriteString(line + "\n") + } + return buf.Bytes() + } + + got := toBytes(out) + + // update golden file if necessary + golden := filepath.Join(fixtureDir, fmt.Sprintf("%s.golden", ts.file)) + + if *update { + err := ioutil.WriteFile(golden, got, 0644) + if err != nil { + t.Error(err) + } + return + } + + // get golden file + want, err := ioutil.ReadFile(golden) + if err != nil { + t.Fatal(err) + } + + from, err := ioutil.ReadFile(filePath) + if err != nil { + t.Fatal(err) + } + + // compare + if !bytes.Equal(got, want) { + t.Errorf("case %s\ngot:\n====\n\n%s\nwant:\n=====\n\n%s\nfrom:\n=====\n\n%s\n", + ts.file, got, want, from) + } + + }) + } +} diff --git a/test-fixtures/all_structs.golden b/internal/gomodifytags/test-fixtures/all_structs.golden similarity index 100% rename from test-fixtures/all_structs.golden rename to internal/gomodifytags/test-fixtures/all_structs.golden diff --git a/test-fixtures/all_structs.input b/internal/gomodifytags/test-fixtures/all_structs.input similarity index 100% rename from test-fixtures/all_structs.input rename to internal/gomodifytags/test-fixtures/all_structs.input diff --git a/test-fixtures/errors.golden b/internal/gomodifytags/test-fixtures/errors.golden similarity index 100% rename from test-fixtures/errors.golden rename to internal/gomodifytags/test-fixtures/errors.golden diff --git a/test-fixtures/errors.input b/internal/gomodifytags/test-fixtures/errors.input similarity index 100% rename from test-fixtures/errors.input rename to internal/gomodifytags/test-fixtures/errors.input diff --git a/test-fixtures/field_add.golden b/internal/gomodifytags/test-fixtures/field_add.golden similarity index 100% rename from test-fixtures/field_add.golden rename to internal/gomodifytags/test-fixtures/field_add.golden diff --git a/test-fixtures/field_add.input b/internal/gomodifytags/test-fixtures/field_add.input similarity index 100% rename from test-fixtures/field_add.input rename to internal/gomodifytags/test-fixtures/field_add.input diff --git a/test-fixtures/field_add_existing.golden b/internal/gomodifytags/test-fixtures/field_add_existing.golden similarity index 100% rename from test-fixtures/field_add_existing.golden rename to internal/gomodifytags/test-fixtures/field_add_existing.golden diff --git a/test-fixtures/field_add_existing.input b/internal/gomodifytags/test-fixtures/field_add_existing.input similarity index 100% rename from test-fixtures/field_add_existing.input rename to internal/gomodifytags/test-fixtures/field_add_existing.input diff --git a/test-fixtures/field_add_same_line.golden b/internal/gomodifytags/test-fixtures/field_add_same_line.golden similarity index 100% rename from test-fixtures/field_add_same_line.golden rename to internal/gomodifytags/test-fixtures/field_add_same_line.golden diff --git a/test-fixtures/field_add_same_line.input b/internal/gomodifytags/test-fixtures/field_add_same_line.input similarity index 100% rename from test-fixtures/field_add_same_line.input rename to internal/gomodifytags/test-fixtures/field_add_same_line.input diff --git a/test-fixtures/field_clear_options.golden b/internal/gomodifytags/test-fixtures/field_clear_options.golden similarity index 100% rename from test-fixtures/field_clear_options.golden rename to internal/gomodifytags/test-fixtures/field_clear_options.golden diff --git a/test-fixtures/field_clear_options.input b/internal/gomodifytags/test-fixtures/field_clear_options.input similarity index 100% rename from test-fixtures/field_clear_options.input rename to internal/gomodifytags/test-fixtures/field_clear_options.input diff --git a/test-fixtures/field_clear_tags.golden b/internal/gomodifytags/test-fixtures/field_clear_tags.golden similarity index 100% rename from test-fixtures/field_clear_tags.golden rename to internal/gomodifytags/test-fixtures/field_clear_tags.golden diff --git a/test-fixtures/field_clear_tags.input b/internal/gomodifytags/test-fixtures/field_clear_tags.input similarity index 100% rename from test-fixtures/field_clear_tags.input rename to internal/gomodifytags/test-fixtures/field_clear_tags.input diff --git a/test-fixtures/field_remove.golden b/internal/gomodifytags/test-fixtures/field_remove.golden similarity index 100% rename from test-fixtures/field_remove.golden rename to internal/gomodifytags/test-fixtures/field_remove.golden diff --git a/test-fixtures/field_remove.input b/internal/gomodifytags/test-fixtures/field_remove.input similarity index 100% rename from test-fixtures/field_remove.input rename to internal/gomodifytags/test-fixtures/field_remove.input diff --git a/test-fixtures/json_all_structs.golden b/internal/gomodifytags/test-fixtures/json_all_structs.golden similarity index 100% rename from test-fixtures/json_all_structs.golden rename to internal/gomodifytags/test-fixtures/json_all_structs.golden diff --git a/test-fixtures/json_all_structs.input b/internal/gomodifytags/test-fixtures/json_all_structs.input similarity index 100% rename from test-fixtures/json_all_structs.input rename to internal/gomodifytags/test-fixtures/json_all_structs.input diff --git a/test-fixtures/json_errors.golden b/internal/gomodifytags/test-fixtures/json_errors.golden similarity index 100% rename from test-fixtures/json_errors.golden rename to internal/gomodifytags/test-fixtures/json_errors.golden diff --git a/test-fixtures/json_errors.input b/internal/gomodifytags/test-fixtures/json_errors.input similarity index 100% rename from test-fixtures/json_errors.input rename to internal/gomodifytags/test-fixtures/json_errors.input diff --git a/test-fixtures/json_full.golden b/internal/gomodifytags/test-fixtures/json_full.golden similarity index 100% rename from test-fixtures/json_full.golden rename to internal/gomodifytags/test-fixtures/json_full.golden diff --git a/test-fixtures/json_full.input b/internal/gomodifytags/test-fixtures/json_full.input similarity index 100% rename from test-fixtures/json_full.input rename to internal/gomodifytags/test-fixtures/json_full.input diff --git a/test-fixtures/json_intersection.golden b/internal/gomodifytags/test-fixtures/json_intersection.golden similarity index 100% rename from test-fixtures/json_intersection.golden rename to internal/gomodifytags/test-fixtures/json_intersection.golden diff --git a/test-fixtures/json_intersection.input b/internal/gomodifytags/test-fixtures/json_intersection.input similarity index 100% rename from test-fixtures/json_intersection.input rename to internal/gomodifytags/test-fixtures/json_intersection.input diff --git a/test-fixtures/json_not_formatted.golden b/internal/gomodifytags/test-fixtures/json_not_formatted.golden similarity index 100% rename from test-fixtures/json_not_formatted.golden rename to internal/gomodifytags/test-fixtures/json_not_formatted.golden diff --git a/test-fixtures/json_not_formatted.input b/internal/gomodifytags/test-fixtures/json_not_formatted.input similarity index 100% rename from test-fixtures/json_not_formatted.input rename to internal/gomodifytags/test-fixtures/json_not_formatted.input diff --git a/test-fixtures/json_not_formatted_2.golden b/internal/gomodifytags/test-fixtures/json_not_formatted_2.golden similarity index 100% rename from test-fixtures/json_not_formatted_2.golden rename to internal/gomodifytags/test-fixtures/json_not_formatted_2.golden diff --git a/test-fixtures/json_not_formatted_2.input b/internal/gomodifytags/test-fixtures/json_not_formatted_2.input similarity index 100% rename from test-fixtures/json_not_formatted_2.input rename to internal/gomodifytags/test-fixtures/json_not_formatted_2.input diff --git a/test-fixtures/json_not_formatted_3.golden b/internal/gomodifytags/test-fixtures/json_not_formatted_3.golden similarity index 100% rename from test-fixtures/json_not_formatted_3.golden rename to internal/gomodifytags/test-fixtures/json_not_formatted_3.golden diff --git a/test-fixtures/json_not_formatted_3.input b/internal/gomodifytags/test-fixtures/json_not_formatted_3.input similarity index 100% rename from test-fixtures/json_not_formatted_3.input rename to internal/gomodifytags/test-fixtures/json_not_formatted_3.input diff --git a/test-fixtures/json_not_formatted_4.golden b/internal/gomodifytags/test-fixtures/json_not_formatted_4.golden similarity index 100% rename from test-fixtures/json_not_formatted_4.golden rename to internal/gomodifytags/test-fixtures/json_not_formatted_4.golden diff --git a/test-fixtures/json_not_formatted_4.input b/internal/gomodifytags/test-fixtures/json_not_formatted_4.input similarity index 100% rename from test-fixtures/json_not_formatted_4.input rename to internal/gomodifytags/test-fixtures/json_not_formatted_4.input diff --git a/test-fixtures/json_not_formatted_5.golden b/internal/gomodifytags/test-fixtures/json_not_formatted_5.golden similarity index 100% rename from test-fixtures/json_not_formatted_5.golden rename to internal/gomodifytags/test-fixtures/json_not_formatted_5.golden diff --git a/test-fixtures/json_not_formatted_5.input b/internal/gomodifytags/test-fixtures/json_not_formatted_5.input similarity index 100% rename from test-fixtures/json_not_formatted_5.input rename to internal/gomodifytags/test-fixtures/json_not_formatted_5.input diff --git a/test-fixtures/json_not_formatted_6.golden b/internal/gomodifytags/test-fixtures/json_not_formatted_6.golden similarity index 100% rename from test-fixtures/json_not_formatted_6.golden rename to internal/gomodifytags/test-fixtures/json_not_formatted_6.golden diff --git a/test-fixtures/json_not_formatted_6.input b/internal/gomodifytags/test-fixtures/json_not_formatted_6.input similarity index 100% rename from test-fixtures/json_not_formatted_6.input rename to internal/gomodifytags/test-fixtures/json_not_formatted_6.input diff --git a/test-fixtures/json_single.golden b/internal/gomodifytags/test-fixtures/json_single.golden similarity index 100% rename from test-fixtures/json_single.golden rename to internal/gomodifytags/test-fixtures/json_single.golden diff --git a/test-fixtures/json_single.input b/internal/gomodifytags/test-fixtures/json_single.input similarity index 100% rename from test-fixtures/json_single.input rename to internal/gomodifytags/test-fixtures/json_single.input diff --git a/test-fixtures/line_add.golden b/internal/gomodifytags/test-fixtures/line_add.golden similarity index 100% rename from test-fixtures/line_add.golden rename to internal/gomodifytags/test-fixtures/line_add.golden diff --git a/test-fixtures/line_add.input b/internal/gomodifytags/test-fixtures/line_add.input similarity index 100% rename from test-fixtures/line_add.input rename to internal/gomodifytags/test-fixtures/line_add.input diff --git a/test-fixtures/line_add_comment.golden b/internal/gomodifytags/test-fixtures/line_add_comment.golden similarity index 100% rename from test-fixtures/line_add_comment.golden rename to internal/gomodifytags/test-fixtures/line_add_comment.golden diff --git a/test-fixtures/line_add_comment.input b/internal/gomodifytags/test-fixtures/line_add_comment.input similarity index 100% rename from test-fixtures/line_add_comment.input rename to internal/gomodifytags/test-fixtures/line_add_comment.input diff --git a/test-fixtures/line_add_intersect_partial.golden b/internal/gomodifytags/test-fixtures/line_add_intersect_partial.golden similarity index 100% rename from test-fixtures/line_add_intersect_partial.golden rename to internal/gomodifytags/test-fixtures/line_add_intersect_partial.golden diff --git a/test-fixtures/line_add_intersect_partial.input b/internal/gomodifytags/test-fixtures/line_add_intersect_partial.input similarity index 100% rename from test-fixtures/line_add_intersect_partial.input rename to internal/gomodifytags/test-fixtures/line_add_intersect_partial.input diff --git a/test-fixtures/line_add_multiple_option.golden b/internal/gomodifytags/test-fixtures/line_add_multiple_option.golden similarity index 100% rename from test-fixtures/line_add_multiple_option.golden rename to internal/gomodifytags/test-fixtures/line_add_multiple_option.golden diff --git a/test-fixtures/line_add_multiple_option.input b/internal/gomodifytags/test-fixtures/line_add_multiple_option.input similarity index 100% rename from test-fixtures/line_add_multiple_option.input rename to internal/gomodifytags/test-fixtures/line_add_multiple_option.input diff --git a/test-fixtures/line_add_no_override.golden b/internal/gomodifytags/test-fixtures/line_add_no_override.golden similarity index 100% rename from test-fixtures/line_add_no_override.golden rename to internal/gomodifytags/test-fixtures/line_add_no_override.golden diff --git a/test-fixtures/line_add_no_override.input b/internal/gomodifytags/test-fixtures/line_add_no_override.input similarity index 100% rename from test-fixtures/line_add_no_override.input rename to internal/gomodifytags/test-fixtures/line_add_no_override.input diff --git a/test-fixtures/line_add_option.golden b/internal/gomodifytags/test-fixtures/line_add_option.golden similarity index 100% rename from test-fixtures/line_add_option.golden rename to internal/gomodifytags/test-fixtures/line_add_option.golden diff --git a/test-fixtures/line_add_option.input b/internal/gomodifytags/test-fixtures/line_add_option.input similarity index 100% rename from test-fixtures/line_add_option.input rename to internal/gomodifytags/test-fixtures/line_add_option.input diff --git a/test-fixtures/line_add_option_existing.golden b/internal/gomodifytags/test-fixtures/line_add_option_existing.golden similarity index 100% rename from test-fixtures/line_add_option_existing.golden rename to internal/gomodifytags/test-fixtures/line_add_option_existing.golden diff --git a/test-fixtures/line_add_option_existing.input b/internal/gomodifytags/test-fixtures/line_add_option_existing.input similarity index 100% rename from test-fixtures/line_add_option_existing.input rename to internal/gomodifytags/test-fixtures/line_add_option_existing.input diff --git a/test-fixtures/line_add_option_with_equal.golden b/internal/gomodifytags/test-fixtures/line_add_option_with_equal.golden similarity index 100% rename from test-fixtures/line_add_option_with_equal.golden rename to internal/gomodifytags/test-fixtures/line_add_option_with_equal.golden diff --git a/test-fixtures/line_add_option_with_equal.input b/internal/gomodifytags/test-fixtures/line_add_option_with_equal.input similarity index 100% rename from test-fixtures/line_add_option_with_equal.input rename to internal/gomodifytags/test-fixtures/line_add_option_with_equal.input diff --git a/test-fixtures/line_add_outside.golden b/internal/gomodifytags/test-fixtures/line_add_outside.golden similarity index 100% rename from test-fixtures/line_add_outside.golden rename to internal/gomodifytags/test-fixtures/line_add_outside.golden diff --git a/test-fixtures/line_add_outside.input b/internal/gomodifytags/test-fixtures/line_add_outside.input similarity index 100% rename from test-fixtures/line_add_outside.input rename to internal/gomodifytags/test-fixtures/line_add_outside.input diff --git a/test-fixtures/line_add_outside_partial_end.golden b/internal/gomodifytags/test-fixtures/line_add_outside_partial_end.golden similarity index 100% rename from test-fixtures/line_add_outside_partial_end.golden rename to internal/gomodifytags/test-fixtures/line_add_outside_partial_end.golden diff --git a/test-fixtures/line_add_outside_partial_end.input b/internal/gomodifytags/test-fixtures/line_add_outside_partial_end.input similarity index 100% rename from test-fixtures/line_add_outside_partial_end.input rename to internal/gomodifytags/test-fixtures/line_add_outside_partial_end.input diff --git a/test-fixtures/line_add_outside_partial_start.golden b/internal/gomodifytags/test-fixtures/line_add_outside_partial_start.golden similarity index 100% rename from test-fixtures/line_add_outside_partial_start.golden rename to internal/gomodifytags/test-fixtures/line_add_outside_partial_start.golden diff --git a/test-fixtures/line_add_outside_partial_start.input b/internal/gomodifytags/test-fixtures/line_add_outside_partial_start.input similarity index 100% rename from test-fixtures/line_add_outside_partial_start.input rename to internal/gomodifytags/test-fixtures/line_add_outside_partial_start.input diff --git a/test-fixtures/line_add_override.golden b/internal/gomodifytags/test-fixtures/line_add_override.golden similarity index 100% rename from test-fixtures/line_add_override.golden rename to internal/gomodifytags/test-fixtures/line_add_override.golden diff --git a/test-fixtures/line_add_override.input b/internal/gomodifytags/test-fixtures/line_add_override.input similarity index 100% rename from test-fixtures/line_add_override.input rename to internal/gomodifytags/test-fixtures/line_add_override.input diff --git a/test-fixtures/line_add_override_column.golden b/internal/gomodifytags/test-fixtures/line_add_override_column.golden similarity index 100% rename from test-fixtures/line_add_override_column.golden rename to internal/gomodifytags/test-fixtures/line_add_override_column.golden diff --git a/test-fixtures/line_add_override_column.input b/internal/gomodifytags/test-fixtures/line_add_override_column.input similarity index 100% rename from test-fixtures/line_add_override_column.input rename to internal/gomodifytags/test-fixtures/line_add_override_column.input diff --git a/test-fixtures/line_add_override_mixed_column_and_equal.golden b/internal/gomodifytags/test-fixtures/line_add_override_mixed_column_and_equal.golden similarity index 100% rename from test-fixtures/line_add_override_mixed_column_and_equal.golden rename to internal/gomodifytags/test-fixtures/line_add_override_mixed_column_and_equal.golden diff --git a/test-fixtures/line_add_override_mixed_column_and_equal.input b/internal/gomodifytags/test-fixtures/line_add_override_mixed_column_and_equal.input similarity index 100% rename from test-fixtures/line_add_override_mixed_column_and_equal.input rename to internal/gomodifytags/test-fixtures/line_add_override_mixed_column_and_equal.input diff --git a/test-fixtures/line_add_override_multi_column.golden b/internal/gomodifytags/test-fixtures/line_add_override_multi_column.golden similarity index 100% rename from test-fixtures/line_add_override_multi_column.golden rename to internal/gomodifytags/test-fixtures/line_add_override_multi_column.golden diff --git a/test-fixtures/line_add_override_multi_column.input b/internal/gomodifytags/test-fixtures/line_add_override_multi_column.input similarity index 100% rename from test-fixtures/line_add_override_multi_column.input rename to internal/gomodifytags/test-fixtures/line_add_override_multi_column.input diff --git a/test-fixtures/line_add_override_multi_equal.golden b/internal/gomodifytags/test-fixtures/line_add_override_multi_equal.golden similarity index 100% rename from test-fixtures/line_add_override_multi_equal.golden rename to internal/gomodifytags/test-fixtures/line_add_override_multi_equal.golden diff --git a/test-fixtures/line_add_override_multi_equal.input b/internal/gomodifytags/test-fixtures/line_add_override_multi_equal.input similarity index 100% rename from test-fixtures/line_add_override_multi_equal.input rename to internal/gomodifytags/test-fixtures/line_add_override_multi_equal.input diff --git a/test-fixtures/line_camelcase_add.golden b/internal/gomodifytags/test-fixtures/line_camelcase_add.golden similarity index 100% rename from test-fixtures/line_camelcase_add.golden rename to internal/gomodifytags/test-fixtures/line_camelcase_add.golden diff --git a/test-fixtures/line_camelcase_add.input b/internal/gomodifytags/test-fixtures/line_camelcase_add.input similarity index 100% rename from test-fixtures/line_camelcase_add.input rename to internal/gomodifytags/test-fixtures/line_camelcase_add.input diff --git a/test-fixtures/line_camelcase_add_embedded.golden b/internal/gomodifytags/test-fixtures/line_camelcase_add_embedded.golden similarity index 100% rename from test-fixtures/line_camelcase_add_embedded.golden rename to internal/gomodifytags/test-fixtures/line_camelcase_add_embedded.golden diff --git a/test-fixtures/line_camelcase_add_embedded.input b/internal/gomodifytags/test-fixtures/line_camelcase_add_embedded.input similarity index 100% rename from test-fixtures/line_camelcase_add_embedded.input rename to internal/gomodifytags/test-fixtures/line_camelcase_add_embedded.input diff --git a/test-fixtures/line_directive_unix.golden b/internal/gomodifytags/test-fixtures/line_directive_unix.golden similarity index 100% rename from test-fixtures/line_directive_unix.golden rename to internal/gomodifytags/test-fixtures/line_directive_unix.golden diff --git a/test-fixtures/line_directive_unix.input b/internal/gomodifytags/test-fixtures/line_directive_unix.input similarity index 100% rename from test-fixtures/line_directive_unix.input rename to internal/gomodifytags/test-fixtures/line_directive_unix.input diff --git a/test-fixtures/line_directive_windows.golden b/internal/gomodifytags/test-fixtures/line_directive_windows.golden similarity index 100% rename from test-fixtures/line_directive_windows.golden rename to internal/gomodifytags/test-fixtures/line_directive_windows.golden diff --git a/test-fixtures/line_directive_windows.input b/internal/gomodifytags/test-fixtures/line_directive_windows.input similarity index 100% rename from test-fixtures/line_directive_windows.input rename to internal/gomodifytags/test-fixtures/line_directive_windows.input diff --git a/test-fixtures/line_lispcase_add.golden b/internal/gomodifytags/test-fixtures/line_lispcase_add.golden similarity index 100% rename from test-fixtures/line_lispcase_add.golden rename to internal/gomodifytags/test-fixtures/line_lispcase_add.golden diff --git a/test-fixtures/line_lispcase_add.input b/internal/gomodifytags/test-fixtures/line_lispcase_add.input similarity index 100% rename from test-fixtures/line_lispcase_add.input rename to internal/gomodifytags/test-fixtures/line_lispcase_add.input diff --git a/test-fixtures/line_multiple_add.golden b/internal/gomodifytags/test-fixtures/line_multiple_add.golden similarity index 100% rename from test-fixtures/line_multiple_add.golden rename to internal/gomodifytags/test-fixtures/line_multiple_add.golden diff --git a/test-fixtures/line_multiple_add.input b/internal/gomodifytags/test-fixtures/line_multiple_add.input similarity index 100% rename from test-fixtures/line_multiple_add.input rename to internal/gomodifytags/test-fixtures/line_multiple_add.input diff --git a/test-fixtures/line_pascalcase_add.golden b/internal/gomodifytags/test-fixtures/line_pascalcase_add.golden similarity index 100% rename from test-fixtures/line_pascalcase_add.golden rename to internal/gomodifytags/test-fixtures/line_pascalcase_add.golden diff --git a/test-fixtures/line_pascalcase_add.input b/internal/gomodifytags/test-fixtures/line_pascalcase_add.input similarity index 100% rename from test-fixtures/line_pascalcase_add.input rename to internal/gomodifytags/test-fixtures/line_pascalcase_add.input diff --git a/test-fixtures/line_pascalcase_add_embedded.golden b/internal/gomodifytags/test-fixtures/line_pascalcase_add_embedded.golden similarity index 100% rename from test-fixtures/line_pascalcase_add_embedded.golden rename to internal/gomodifytags/test-fixtures/line_pascalcase_add_embedded.golden diff --git a/test-fixtures/line_pascalcase_add_embedded.input b/internal/gomodifytags/test-fixtures/line_pascalcase_add_embedded.input similarity index 100% rename from test-fixtures/line_pascalcase_add_embedded.input rename to internal/gomodifytags/test-fixtures/line_pascalcase_add_embedded.input diff --git a/test-fixtures/line_remove.golden b/internal/gomodifytags/test-fixtures/line_remove.golden similarity index 100% rename from test-fixtures/line_remove.golden rename to internal/gomodifytags/test-fixtures/line_remove.golden diff --git a/test-fixtures/line_remove.input b/internal/gomodifytags/test-fixtures/line_remove.input similarity index 100% rename from test-fixtures/line_remove.input rename to internal/gomodifytags/test-fixtures/line_remove.input diff --git a/test-fixtures/line_remove_option.golden b/internal/gomodifytags/test-fixtures/line_remove_option.golden similarity index 100% rename from test-fixtures/line_remove_option.golden rename to internal/gomodifytags/test-fixtures/line_remove_option.golden diff --git a/test-fixtures/line_remove_option.input b/internal/gomodifytags/test-fixtures/line_remove_option.input similarity index 100% rename from test-fixtures/line_remove_option.input rename to internal/gomodifytags/test-fixtures/line_remove_option.input diff --git a/test-fixtures/line_remove_option_with_equal.golden b/internal/gomodifytags/test-fixtures/line_remove_option_with_equal.golden similarity index 100% rename from test-fixtures/line_remove_option_with_equal.golden rename to internal/gomodifytags/test-fixtures/line_remove_option_with_equal.golden diff --git a/test-fixtures/line_remove_option_with_equal.input b/internal/gomodifytags/test-fixtures/line_remove_option_with_equal.input similarity index 100% rename from test-fixtures/line_remove_option_with_equal.input rename to internal/gomodifytags/test-fixtures/line_remove_option_with_equal.input diff --git a/test-fixtures/line_remove_options.golden b/internal/gomodifytags/test-fixtures/line_remove_options.golden similarity index 100% rename from test-fixtures/line_remove_options.golden rename to internal/gomodifytags/test-fixtures/line_remove_options.golden diff --git a/test-fixtures/line_remove_options.input b/internal/gomodifytags/test-fixtures/line_remove_options.input similarity index 100% rename from test-fixtures/line_remove_options.input rename to internal/gomodifytags/test-fixtures/line_remove_options.input diff --git a/test-fixtures/line_titlecase_add.golden b/internal/gomodifytags/test-fixtures/line_titlecase_add.golden similarity index 100% rename from test-fixtures/line_titlecase_add.golden rename to internal/gomodifytags/test-fixtures/line_titlecase_add.golden diff --git a/test-fixtures/line_titlecase_add.input b/internal/gomodifytags/test-fixtures/line_titlecase_add.input similarity index 100% rename from test-fixtures/line_titlecase_add.input rename to internal/gomodifytags/test-fixtures/line_titlecase_add.input diff --git a/test-fixtures/line_titlecase_add_embedded.golden b/internal/gomodifytags/test-fixtures/line_titlecase_add_embedded.golden similarity index 100% rename from test-fixtures/line_titlecase_add_embedded.golden rename to internal/gomodifytags/test-fixtures/line_titlecase_add_embedded.golden diff --git a/test-fixtures/line_titlecase_add_embedded.input b/internal/gomodifytags/test-fixtures/line_titlecase_add_embedded.input similarity index 100% rename from test-fixtures/line_titlecase_add_embedded.input rename to internal/gomodifytags/test-fixtures/line_titlecase_add_embedded.input diff --git a/test-fixtures/line_value_add.golden b/internal/gomodifytags/test-fixtures/line_value_add.golden similarity index 100% rename from test-fixtures/line_value_add.golden rename to internal/gomodifytags/test-fixtures/line_value_add.golden diff --git a/test-fixtures/line_value_add.input b/internal/gomodifytags/test-fixtures/line_value_add.input similarity index 100% rename from test-fixtures/line_value_add.input rename to internal/gomodifytags/test-fixtures/line_value_add.input diff --git a/test-fixtures/not_formatted.golden b/internal/gomodifytags/test-fixtures/not_formatted.golden similarity index 100% rename from test-fixtures/not_formatted.golden rename to internal/gomodifytags/test-fixtures/not_formatted.golden diff --git a/test-fixtures/not_formatted.input b/internal/gomodifytags/test-fixtures/not_formatted.input similarity index 100% rename from test-fixtures/not_formatted.input rename to internal/gomodifytags/test-fixtures/not_formatted.input diff --git a/test-fixtures/offset_add.golden b/internal/gomodifytags/test-fixtures/offset_add.golden similarity index 100% rename from test-fixtures/offset_add.golden rename to internal/gomodifytags/test-fixtures/offset_add.golden diff --git a/test-fixtures/offset_add.input b/internal/gomodifytags/test-fixtures/offset_add.input similarity index 100% rename from test-fixtures/offset_add.input rename to internal/gomodifytags/test-fixtures/offset_add.input diff --git a/test-fixtures/offset_add_composite.golden b/internal/gomodifytags/test-fixtures/offset_add_composite.golden similarity index 100% rename from test-fixtures/offset_add_composite.golden rename to internal/gomodifytags/test-fixtures/offset_add_composite.golden diff --git a/test-fixtures/offset_add_composite.input b/internal/gomodifytags/test-fixtures/offset_add_composite.input similarity index 100% rename from test-fixtures/offset_add_composite.input rename to internal/gomodifytags/test-fixtures/offset_add_composite.input diff --git a/test-fixtures/offset_add_duplicate.golden b/internal/gomodifytags/test-fixtures/offset_add_duplicate.golden similarity index 100% rename from test-fixtures/offset_add_duplicate.golden rename to internal/gomodifytags/test-fixtures/offset_add_duplicate.golden diff --git a/test-fixtures/offset_add_duplicate.input b/internal/gomodifytags/test-fixtures/offset_add_duplicate.input similarity index 100% rename from test-fixtures/offset_add_duplicate.input rename to internal/gomodifytags/test-fixtures/offset_add_duplicate.input diff --git a/test-fixtures/offset_add_literal_in.golden b/internal/gomodifytags/test-fixtures/offset_add_literal_in.golden similarity index 100% rename from test-fixtures/offset_add_literal_in.golden rename to internal/gomodifytags/test-fixtures/offset_add_literal_in.golden diff --git a/test-fixtures/offset_add_literal_in.input b/internal/gomodifytags/test-fixtures/offset_add_literal_in.input similarity index 100% rename from test-fixtures/offset_add_literal_in.input rename to internal/gomodifytags/test-fixtures/offset_add_literal_in.input diff --git a/test-fixtures/offset_add_literal_out.golden b/internal/gomodifytags/test-fixtures/offset_add_literal_out.golden similarity index 100% rename from test-fixtures/offset_add_literal_out.golden rename to internal/gomodifytags/test-fixtures/offset_add_literal_out.golden diff --git a/test-fixtures/offset_add_literal_out.input b/internal/gomodifytags/test-fixtures/offset_add_literal_out.input similarity index 100% rename from test-fixtures/offset_add_literal_out.input rename to internal/gomodifytags/test-fixtures/offset_add_literal_out.input diff --git a/test-fixtures/offset_anonymous_struct.golden b/internal/gomodifytags/test-fixtures/offset_anonymous_struct.golden similarity index 100% rename from test-fixtures/offset_anonymous_struct.golden rename to internal/gomodifytags/test-fixtures/offset_anonymous_struct.golden diff --git a/test-fixtures/offset_anonymous_struct.input b/internal/gomodifytags/test-fixtures/offset_anonymous_struct.input similarity index 100% rename from test-fixtures/offset_anonymous_struct.input rename to internal/gomodifytags/test-fixtures/offset_anonymous_struct.input diff --git a/test-fixtures/offset_array_struct.golden b/internal/gomodifytags/test-fixtures/offset_array_struct.golden similarity index 100% rename from test-fixtures/offset_array_struct.golden rename to internal/gomodifytags/test-fixtures/offset_array_struct.golden diff --git a/test-fixtures/offset_array_struct.input b/internal/gomodifytags/test-fixtures/offset_array_struct.input similarity index 100% rename from test-fixtures/offset_array_struct.input rename to internal/gomodifytags/test-fixtures/offset_array_struct.input diff --git a/test-fixtures/offset_star_struct.golden b/internal/gomodifytags/test-fixtures/offset_star_struct.golden similarity index 100% rename from test-fixtures/offset_star_struct.golden rename to internal/gomodifytags/test-fixtures/offset_star_struct.golden diff --git a/test-fixtures/offset_star_struct.input b/internal/gomodifytags/test-fixtures/offset_star_struct.input similarity index 100% rename from test-fixtures/offset_star_struct.input rename to internal/gomodifytags/test-fixtures/offset_star_struct.input diff --git a/test-fixtures/skip_embedded.golden b/internal/gomodifytags/test-fixtures/skip_embedded.golden similarity index 100% rename from test-fixtures/skip_embedded.golden rename to internal/gomodifytags/test-fixtures/skip_embedded.golden diff --git a/test-fixtures/skip_embedded.input b/internal/gomodifytags/test-fixtures/skip_embedded.input similarity index 100% rename from test-fixtures/skip_embedded.input rename to internal/gomodifytags/test-fixtures/skip_embedded.input diff --git a/test-fixtures/skip_private.golden b/internal/gomodifytags/test-fixtures/skip_private.golden similarity index 100% rename from test-fixtures/skip_private.golden rename to internal/gomodifytags/test-fixtures/skip_private.golden diff --git a/test-fixtures/skip_private.input b/internal/gomodifytags/test-fixtures/skip_private.input similarity index 100% rename from test-fixtures/skip_private.input rename to internal/gomodifytags/test-fixtures/skip_private.input diff --git a/test-fixtures/skip_private_multiple_names.golden b/internal/gomodifytags/test-fixtures/skip_private_multiple_names.golden similarity index 100% rename from test-fixtures/skip_private_multiple_names.golden rename to internal/gomodifytags/test-fixtures/skip_private_multiple_names.golden diff --git a/test-fixtures/skip_private_multiple_names.input b/internal/gomodifytags/test-fixtures/skip_private_multiple_names.input similarity index 100% rename from test-fixtures/skip_private_multiple_names.input rename to internal/gomodifytags/test-fixtures/skip_private_multiple_names.input diff --git a/test-fixtures/struct_add.golden b/internal/gomodifytags/test-fixtures/struct_add.golden similarity index 100% rename from test-fixtures/struct_add.golden rename to internal/gomodifytags/test-fixtures/struct_add.golden diff --git a/test-fixtures/struct_add.input b/internal/gomodifytags/test-fixtures/struct_add.input similarity index 100% rename from test-fixtures/struct_add.input rename to internal/gomodifytags/test-fixtures/struct_add.input diff --git a/test-fixtures/struct_add_existing.golden b/internal/gomodifytags/test-fixtures/struct_add_existing.golden similarity index 100% rename from test-fixtures/struct_add_existing.golden rename to internal/gomodifytags/test-fixtures/struct_add_existing.golden diff --git a/test-fixtures/struct_add_existing.input b/internal/gomodifytags/test-fixtures/struct_add_existing.input similarity index 100% rename from test-fixtures/struct_add_existing.input rename to internal/gomodifytags/test-fixtures/struct_add_existing.input diff --git a/test-fixtures/struct_clear_options.golden b/internal/gomodifytags/test-fixtures/struct_clear_options.golden similarity index 100% rename from test-fixtures/struct_clear_options.golden rename to internal/gomodifytags/test-fixtures/struct_clear_options.golden diff --git a/test-fixtures/struct_clear_options.input b/internal/gomodifytags/test-fixtures/struct_clear_options.input similarity index 100% rename from test-fixtures/struct_clear_options.input rename to internal/gomodifytags/test-fixtures/struct_clear_options.input diff --git a/test-fixtures/struct_clear_tags.golden b/internal/gomodifytags/test-fixtures/struct_clear_tags.golden similarity index 100% rename from test-fixtures/struct_clear_tags.golden rename to internal/gomodifytags/test-fixtures/struct_clear_tags.golden diff --git a/test-fixtures/struct_clear_tags.input b/internal/gomodifytags/test-fixtures/struct_clear_tags.input similarity index 100% rename from test-fixtures/struct_clear_tags.input rename to internal/gomodifytags/test-fixtures/struct_clear_tags.input diff --git a/test-fixtures/struct_format.golden b/internal/gomodifytags/test-fixtures/struct_format.golden similarity index 100% rename from test-fixtures/struct_format.golden rename to internal/gomodifytags/test-fixtures/struct_format.golden diff --git a/test-fixtures/struct_format.input b/internal/gomodifytags/test-fixtures/struct_format.input similarity index 100% rename from test-fixtures/struct_format.input rename to internal/gomodifytags/test-fixtures/struct_format.input diff --git a/test-fixtures/struct_format_existing.golden b/internal/gomodifytags/test-fixtures/struct_format_existing.golden similarity index 100% rename from test-fixtures/struct_format_existing.golden rename to internal/gomodifytags/test-fixtures/struct_format_existing.golden diff --git a/test-fixtures/struct_format_existing.input b/internal/gomodifytags/test-fixtures/struct_format_existing.input similarity index 100% rename from test-fixtures/struct_format_existing.input rename to internal/gomodifytags/test-fixtures/struct_format_existing.input diff --git a/test-fixtures/struct_format_existing_oldstyle.golden b/internal/gomodifytags/test-fixtures/struct_format_existing_oldstyle.golden similarity index 100% rename from test-fixtures/struct_format_existing_oldstyle.golden rename to internal/gomodifytags/test-fixtures/struct_format_existing_oldstyle.golden diff --git a/test-fixtures/struct_format_existing_oldstyle.input b/internal/gomodifytags/test-fixtures/struct_format_existing_oldstyle.input similarity index 100% rename from test-fixtures/struct_format_existing_oldstyle.input rename to internal/gomodifytags/test-fixtures/struct_format_existing_oldstyle.input diff --git a/test-fixtures/struct_format_oldstyle.golden b/internal/gomodifytags/test-fixtures/struct_format_oldstyle.golden similarity index 100% rename from test-fixtures/struct_format_oldstyle.golden rename to internal/gomodifytags/test-fixtures/struct_format_oldstyle.golden diff --git a/test-fixtures/struct_format_oldstyle.input b/internal/gomodifytags/test-fixtures/struct_format_oldstyle.input similarity index 100% rename from test-fixtures/struct_format_oldstyle.input rename to internal/gomodifytags/test-fixtures/struct_format_oldstyle.input diff --git a/test-fixtures/struct_remove.golden b/internal/gomodifytags/test-fixtures/struct_remove.golden similarity index 100% rename from test-fixtures/struct_remove.golden rename to internal/gomodifytags/test-fixtures/struct_remove.golden diff --git a/test-fixtures/struct_remove.input b/internal/gomodifytags/test-fixtures/struct_remove.input similarity index 100% rename from test-fixtures/struct_remove.input rename to internal/gomodifytags/test-fixtures/struct_remove.input diff --git a/main.go b/main.go index e4efff7..47fea30 100644 --- a/main.go +++ b/main.go @@ -1,77 +1,14 @@ package main import ( - "bufio" - "bytes" - "encoding/json" - "errors" "flag" "fmt" - "go/ast" - "go/format" - "go/parser" - "go/printer" - "go/token" - "io" - "io/ioutil" "os" - "sort" - "strconv" "strings" - "unicode" - "github.com/fatih/camelcase" - "github.com/fatih/structtag" - "golang.org/x/tools/go/buildutil" + "github.com/fatih/gomodifytags/internal/gomodifytags" ) -// structType contains a structType node and it's name. It's a convenient -// helper type, because *ast.StructType doesn't contain the name of the struct -type structType struct { - name string - node *ast.StructType -} - -// output is used usually by editors -type output struct { - Start int `json:"start"` - End int `json:"end"` - Lines []string `json:"lines"` - Errors []string `json:"errors,omitempty"` -} - -// config defines how tags should be modified -type config struct { - file string - output string - quiet bool - write bool - modified io.Reader - - offset int - structName string - fieldName string - line string - start, end int - all bool - - fset *token.FileSet - - remove []string - removeOptions []string - - add []string - addOptions []string - override bool - skipUnexportedFields bool - - transform string - sort bool - valueFormat string - clear bool - clearOption bool -} - func main() { if err := realMain(); err != nil { fmt.Fprintln(os.Stderr, err.Error()) @@ -88,40 +25,10 @@ func realMain() error { return err } - err = cfg.validate() - if err != nil { - return err - } - - node, err := cfg.parse() - if err != nil { - return err - } - - start, end, err := cfg.findSelection(node) - if err != nil { - return err - } - - rewrittenNode, errs := cfg.rewrite(node, start, end) - if errs != nil { - if _, ok := errs.(*rewriteErrors); !ok { - return errs - } - } - - out, err := cfg.format(rewrittenNode, errs) - if err != nil { - return err - } - - if !cfg.quiet { - fmt.Println(out) - } - return nil + return cfg.Run() } -func parseConfig(args []string) (*config, error) { +func parseConfig(args []string) (*gomodifytags.Config, error) { var ( // file flags flagFile = flag.String("file", "", "Filename to be parsed") @@ -183,740 +90,44 @@ func parseConfig(args []string) (*config, error) { return nil, flag.ErrHelp } - cfg := &config{ - file: *flagFile, - line: *flagLine, - structName: *flagStruct, - fieldName: *flagField, - offset: *flagOffset, - all: *flagAll, - output: *flagOutput, - write: *flagWrite, - quiet: *flagQuiet, - clear: *flagClearTags, - clearOption: *flagClearOptions, - transform: *flagTransform, - sort: *flagSort, - valueFormat: *flagFormatting, - override: *flagOverride, - skipUnexportedFields: *flagSkipUnexportedFields, + cfg := &gomodifytags.Config{ + File: *flagFile, + Line: *flagLine, + StructName: *flagStruct, + FieldName: *flagField, + Offset: *flagOffset, + All: *flagAll, + Output: *flagOutput, + Write: *flagWrite, + Quiet: *flagQuiet, + Clear: *flagClearTags, + ClearOption: *flagClearOptions, + Transform: *flagTransform, + Sort: *flagSort, + ValueFormat: *flagFormatting, + Override: *flagOverride, + SkipUnexportedFields: *flagSkipUnexportedFields, } if *flagModified { - cfg.modified = os.Stdin + cfg.Modified = os.Stdin } if *flagAddTags != "" { - cfg.add = strings.Split(*flagAddTags, ",") + cfg.Add = strings.Split(*flagAddTags, ",") } if *flagAddOptions != "" { - cfg.addOptions = strings.Split(*flagAddOptions, ",") + cfg.AddOptions = strings.Split(*flagAddOptions, ",") } if *flagRemoveTags != "" { - cfg.remove = strings.Split(*flagRemoveTags, ",") + cfg.Remove = strings.Split(*flagRemoveTags, ",") } if *flagRemoveOptions != "" { - cfg.removeOptions = strings.Split(*flagRemoveOptions, ",") + cfg.RemoveOptions = strings.Split(*flagRemoveOptions, ",") } return cfg, nil - -} - -func (c *config) parse() (ast.Node, error) { - c.fset = token.NewFileSet() - var contents interface{} - if c.modified != nil { - archive, err := buildutil.ParseOverlayArchive(c.modified) - if err != nil { - return nil, fmt.Errorf("failed to parse -modified archive: %v", err) - } - fc, ok := archive[c.file] - if !ok { - return nil, fmt.Errorf("couldn't find %s in archive", c.file) - } - contents = fc - } - - return parser.ParseFile(c.fset, c.file, contents, parser.ParseComments) -} - -// findSelection returns the start and end position of the fields that are -// suspect to change. It depends on the line, struct or offset selection. -func (c *config) findSelection(node ast.Node) (int, int, error) { - if c.line != "" { - return c.lineSelection(node) - } else if c.offset != 0 { - return c.offsetSelection(node) - } else if c.structName != "" { - return c.structSelection(node) - } else if c.all { - return c.allSelection(node) - } else { - return 0, 0, errors.New("-line, -offset, -struct or -all is not passed") - } -} - -func (c *config) process(fieldName, tagVal string) (string, error) { - var tag string - if tagVal != "" { - var err error - tag, err = strconv.Unquote(tagVal) - if err != nil { - return "", err - } - } - - tags, err := structtag.Parse(tag) - if err != nil { - return "", err - } - - tags = c.removeTags(tags) - tags, err = c.removeTagOptions(tags) - if err != nil { - return "", err - } - - tags = c.clearTags(tags) - tags = c.clearOptions(tags) - - tags, err = c.addTags(fieldName, tags) - if err != nil { - return "", err - } - - tags, err = c.addTagOptions(tags) - if err != nil { - return "", err - } - - if c.sort { - sort.Sort(tags) - } - - res := tags.String() - if res != "" { - res = quote(tags.String()) - } - - return res, nil -} - -func (c *config) removeTags(tags *structtag.Tags) *structtag.Tags { - if c.remove == nil || len(c.remove) == 0 { - return tags - } - - tags.Delete(c.remove...) - return tags -} - -func (c *config) clearTags(tags *structtag.Tags) *structtag.Tags { - if !c.clear { - return tags - } - - tags.Delete(tags.Keys()...) - return tags -} - -func (c *config) clearOptions(tags *structtag.Tags) *structtag.Tags { - if !c.clearOption { - return tags - } - - for _, t := range tags.Tags() { - t.Options = nil - } - - return tags -} - -func (c *config) removeTagOptions(tags *structtag.Tags) (*structtag.Tags, error) { - if c.removeOptions == nil || len(c.removeOptions) == 0 { - return tags, nil - } - - for _, val := range c.removeOptions { - // syntax key=option - splitted := strings.Split(val, "=") - if len(splitted) < 2 { - return nil, errors.New("wrong syntax to remove an option. i.e key=option") - } - - key := splitted[0] - option := strings.Join(splitted[1:], "=") - - tags.DeleteOptions(key, option) - } - - return tags, nil -} - -func (c *config) addTagOptions(tags *structtag.Tags) (*structtag.Tags, error) { - if c.addOptions == nil || len(c.addOptions) == 0 { - return tags, nil - } - - for _, val := range c.addOptions { - // syntax key=option - splitted := strings.Split(val, "=") - if len(splitted) < 2 { - return nil, errors.New("wrong syntax to add an option. i.e key=option") - } - - key := splitted[0] - option := strings.Join(splitted[1:], "=") - - tags.AddOptions(key, option) - } - - return tags, nil -} - -func (c *config) addTags(fieldName string, tags *structtag.Tags) (*structtag.Tags, error) { - if c.add == nil || len(c.add) == 0 { - return tags, nil - } - - splitted := camelcase.Split(fieldName) - name := "" - - unknown := false - switch c.transform { - case "snakecase": - var lowerSplitted []string - for _, s := range splitted { - lowerSplitted = append(lowerSplitted, strings.ToLower(s)) - } - - name = strings.Join(lowerSplitted, "_") - case "lispcase": - var lowerSplitted []string - for _, s := range splitted { - lowerSplitted = append(lowerSplitted, strings.ToLower(s)) - } - - name = strings.Join(lowerSplitted, "-") - case "camelcase": - var titled []string - for _, s := range splitted { - titled = append(titled, strings.Title(s)) - } - - titled[0] = strings.ToLower(titled[0]) - - name = strings.Join(titled, "") - case "pascalcase": - var titled []string - for _, s := range splitted { - titled = append(titled, strings.Title(s)) - } - - name = strings.Join(titled, "") - case "titlecase": - var titled []string - for _, s := range splitted { - titled = append(titled, strings.Title(s)) - } - - name = strings.Join(titled, " ") - case "keep": - name = fieldName - default: - unknown = true - } - - if c.valueFormat != "" { - prevName := name - name = strings.ReplaceAll(c.valueFormat, "{field}", name) - if name == c.valueFormat { - // support old style for backward compatibility - name = strings.ReplaceAll(c.valueFormat, "$field", prevName) - } - } - - for _, key := range c.add { - splitted = strings.SplitN(key, ":", 2) - if len(splitted) >= 2 { - key = splitted[0] - name = strings.Join(splitted[1:], "") - } else if unknown { - // the user didn't pass any value but want to use an unknown - // transform. We don't return above in the default as the user - // might pass a value - return nil, fmt.Errorf("unknown transform option %q", c.transform) - } - - tag, err := tags.Get(key) - if err != nil { - // tag doesn't exist, create a new one - tag = &structtag.Tag{ - Key: key, - Name: name, - } - } else if c.override { - tag.Name = name - } - - if err := tags.Set(tag); err != nil { - return nil, err - } - } - - return tags, nil -} - -// collectStructs collects and maps structType nodes to their positions -func collectStructs(node ast.Node) map[token.Pos]*structType { - structs := make(map[token.Pos]*structType, 0) - - collectStructs := func(n ast.Node) bool { - var t ast.Expr - var structName string - - switch x := n.(type) { - case *ast.TypeSpec: - if x.Type == nil { - return true - - } - - structName = x.Name.Name - t = x.Type - case *ast.CompositeLit: - t = x.Type - case *ast.ValueSpec: - structName = x.Names[0].Name - t = x.Type - case *ast.Field: - // this case also catches struct fields and the structName - // therefore might contain the field name (which is wrong) - // because `x.Type` in this case is not a *ast.StructType. - // - // We're OK with it, because, in our case *ast.Field represents - // a parameter declaration, i.e: - // - // func test(arg struct { - // Field int - // }) { - // } - // - // and hence the struct name will be `arg`. - if len(x.Names) != 0 { - structName = x.Names[0].Name - } - t = x.Type - } - - // if expression is in form "*T" or "[]T", dereference to check if "T" - // contains a struct expression - t = deref(t) - - x, ok := t.(*ast.StructType) - if !ok { - return true - } - - structs[x.Pos()] = &structType{ - name: structName, - node: x, - } - return true - } - - ast.Inspect(node, collectStructs) - return structs -} - -func (c *config) format(file ast.Node, rwErrs error) (string, error) { - switch c.output { - case "source": - var buf bytes.Buffer - err := format.Node(&buf, c.fset, file) - if err != nil { - return "", err - } - - if c.write { - err = ioutil.WriteFile(c.file, buf.Bytes(), 0) - if err != nil { - return "", err - } - } - - return buf.String(), nil - case "json": - // NOTE(arslan): print first the whole file and then cut out our - // selection. The reason we don't directly print the struct is that the - // printer is not capable of printing loosy comments, comments that are - // not part of any field inside a struct. Those are part of *ast.File - // and only printed inside a struct if we print the whole file. This - // approach is the sanest and simplest way to get a struct printed - // back. Second, our cursor might intersect two different structs with - // other declarations in between them. Printing the file and cutting - // the selection is the easier and simpler to do. - var buf bytes.Buffer - - // this is the default config from `format.Node()`, but we add - // `printer.SourcePos` to get the original source position of the - // modified lines - cfg := printer.Config{Mode: printer.SourcePos | printer.UseSpaces | printer.TabIndent, Tabwidth: 8} - err := cfg.Fprint(&buf, c.fset, file) - if err != nil { - return "", err - } - - lines, err := parseLines(&buf) - if err != nil { - return "", err - } - - // prevent selection to be larger than the actual number of lines - if c.start > len(lines) || c.end > len(lines) { - return "", errors.New("line selection is invalid") - } - - out := &output{ - Start: c.start, - End: c.end, - Lines: lines[c.start-1 : c.end], - } - - if rwErrs != nil { - if r, ok := rwErrs.(*rewriteErrors); ok { - for _, err := range r.errs { - out.Errors = append(out.Errors, err.Error()) - } - } - } - - o, err := json.MarshalIndent(out, "", " ") - if err != nil { - return "", err - } - - return string(o), nil - default: - return "", fmt.Errorf("unknown output mode: %s", c.output) - } -} - -func (c *config) lineSelection(file ast.Node) (int, int, error) { - var err error - splitted := strings.Split(c.line, ",") - - start, err := strconv.Atoi(splitted[0]) - if err != nil { - return 0, 0, err - } - - end := start - if len(splitted) == 2 { - end, err = strconv.Atoi(splitted[1]) - if err != nil { - return 0, 0, err - } - } - - if start > end { - return 0, 0, errors.New("wrong range. start line cannot be larger than end line") - } - - return start, end, nil -} - -func (c *config) structSelection(file ast.Node) (int, int, error) { - structs := collectStructs(file) - - var encStruct *ast.StructType - for _, st := range structs { - if st.name == c.structName { - encStruct = st.node - } - } - - if encStruct == nil { - return 0, 0, errors.New("struct name does not exist") - } - - // if field name has been specified as well, only select the given field - if c.fieldName != "" { - return c.fieldSelection(encStruct) - } - - start := c.fset.Position(encStruct.Pos()).Line - end := c.fset.Position(encStruct.End()).Line - - return start, end, nil -} - -func (c *config) fieldSelection(st *ast.StructType) (int, int, error) { - var encField *ast.Field - for _, f := range st.Fields.List { - for _, field := range f.Names { - if field.Name == c.fieldName { - encField = f - } - } - } - - if encField == nil { - return 0, 0, errors.New(fmt.Sprintf("struct %q doesn't have field name %q", - c.structName, c.fieldName)) - } - - start := c.fset.Position(encField.Pos()).Line - end := c.fset.Position(encField.End()).Line - - return start, end, nil -} - -func (c *config) offsetSelection(file ast.Node) (int, int, error) { - structs := collectStructs(file) - - var encStruct *ast.StructType - for _, st := range structs { - structBegin := c.fset.Position(st.node.Pos()).Offset - structEnd := c.fset.Position(st.node.End()).Offset - - if structBegin <= c.offset && c.offset <= structEnd { - encStruct = st.node - break - } - } - - if encStruct == nil { - return 0, 0, errors.New("offset is not inside a struct") - } - - // offset selects all fields - start := c.fset.Position(encStruct.Pos()).Line - end := c.fset.Position(encStruct.End()).Line - - return start, end, nil -} - -// allSelection selects all structs inside a file -func (c *config) allSelection(file ast.Node) (int, int, error) { - start := 1 - end := c.fset.File(file.Pos()).LineCount() - - return start, end, nil -} - -func isPublicName(name string) bool { - for _, c := range name { - return unicode.IsUpper(c) - } - return false -} - -// rewrite rewrites the node for structs between the start and end -// positions -func (c *config) rewrite(node ast.Node, start, end int) (ast.Node, error) { - errs := &rewriteErrors{errs: make([]error, 0)} - - rewriteFunc := func(n ast.Node) bool { - x, ok := n.(*ast.StructType) - if !ok { - return true - } - - for _, f := range x.Fields.List { - line := c.fset.Position(f.Pos()).Line - - if !(start <= line && line <= end) { - continue - } - - fieldName := "" - if len(f.Names) != 0 { - for _, field := range f.Names { - if !c.skipUnexportedFields || isPublicName(field.Name) { - fieldName = field.Name - break - } - } - } - - // anonymous field - if f.Names == nil { - ident, ok := f.Type.(*ast.Ident) - if !ok { - continue - } - - if !c.skipUnexportedFields { - fieldName = ident.Name - } - } - - // nothing to process, continue with next line - if fieldName == "" { - continue - } - - if f.Tag == nil { - f.Tag = &ast.BasicLit{} - } - - res, err := c.process(fieldName, f.Tag.Value) - if err != nil { - errs.Append(fmt.Errorf("%s:%d:%d:%s", - c.fset.Position(f.Pos()).Filename, - c.fset.Position(f.Pos()).Line, - c.fset.Position(f.Pos()).Column, - err)) - continue - } - - f.Tag.Value = res - } - - return true - } - - ast.Inspect(node, rewriteFunc) - - c.start = start - c.end = end - - if len(errs.errs) == 0 { - return node, nil - } - - return node, errs -} - -// validate validates whether the config is valid or not -func (c *config) validate() error { - if c.file == "" { - return errors.New("no file is passed") - } - - if c.line == "" && c.offset == 0 && c.structName == "" && !c.all { - return errors.New("-line, -offset, -struct or -all is not passed") - } - - if c.line != "" && c.offset != 0 || - c.line != "" && c.structName != "" || - c.offset != 0 && c.structName != "" { - return errors.New("-line, -offset or -struct cannot be used together. pick one") - } - - if (c.add == nil || len(c.add) == 0) && - (c.addOptions == nil || len(c.addOptions) == 0) && - !c.clear && - !c.clearOption && - (c.removeOptions == nil || len(c.removeOptions) == 0) && - (c.remove == nil || len(c.remove) == 0) { - return errors.New("one of " + - "[-add-tags, -add-options, -remove-tags, -remove-options, -clear-tags, -clear-options]" + - " should be defined") - } - - if c.fieldName != "" && c.structName == "" { - return errors.New("-field is requiring -struct") - } - - return nil -} - -func quote(tag string) string { - return "`" + tag + "`" -} - -type rewriteErrors struct { - errs []error -} - -func (r *rewriteErrors) Error() string { - var buf bytes.Buffer - for _, e := range r.errs { - buf.WriteString(fmt.Sprintf("%s\n", e.Error())) - } - return buf.String() -} - -func (r *rewriteErrors) Append(err error) { - if err == nil { - return - } - - r.errs = append(r.errs, err) -} - -// parseLines parses the given buffer and returns a slice of lines -func parseLines(buf io.Reader) ([]string, error) { - var lines []string - scanner := bufio.NewScanner(buf) - for scanner.Scan() { - txt := scanner.Text() - - // check for any line directive and store it for next iteration to - // re-construct the original file. If it's not a line directive, - // continue consturcting the original file - if !strings.HasPrefix(txt, "//line") { - lines = append(lines, txt) - continue - } - - lineNr, err := split(txt) - if err != nil { - return nil, err - } - - for i := len(lines); i < lineNr-1; i++ { - lines = append(lines, "") - } - - lines = lines[:lineNr-1] - } - - if err := scanner.Err(); err != nil { - return nil, fmt.Errorf("invalid scanner inputl: %s", err) - } - - return lines, nil -} - -// split splits the given line directive and returns the line number -// see https://golang.org/cmd/compile/#hdr-Compiler_Directives for more -// information -// NOTE(arslan): this only splits the line directive that the go.Parser -// outputs. If the go parser changes the format of the line directive, make -// sure to fix it in the below function -func split(line string) (int, error) { - for i := len(line) - 1; i >= 0; i-- { - if line[i] != ':' { - continue - } - - nr, err := strconv.Atoi(line[i+1:]) - if err != nil { - return 0, err - } - - return nr, nil - } - - return 0, fmt.Errorf("couldn't parse line: '%s'", line) -} - -// deref takes an expression, and removes all its leading "*" and "[]" -// operator. Uuse case : if found expression is a "*t" or "[]t", we need to -// check if "t" contains a struct expression. -func deref(x ast.Expr) ast.Expr { - switch t := x.(type) { - case *ast.StarExpr: - return deref(t.X) - case *ast.ArrayType: - return deref(t.Elt) - } - return x } diff --git a/main_test.go b/main_test.go index 1facda4..b9f9205 100644 --- a/main_test.go +++ b/main_test.go @@ -1,935 +1,13 @@ package main import ( - "bytes" - "errors" "flag" - "fmt" "io/ioutil" - "os" - "path/filepath" - "reflect" - "strings" "testing" ) -var update = flag.Bool("update", false, "update golden (.out) files") - -// This is the directory where our test fixtures are. -const fixtureDir = "./test-fixtures" - -func TestRewrite(t *testing.T) { - test := []struct { - cfg *config - file string - }{ - { - file: "struct_add", - cfg: &config{ - add: []string{"json"}, - output: "source", - structName: "foo", - transform: "snakecase", - }, - }, - { - file: "struct_add_existing", - cfg: &config{ - add: []string{"json"}, - output: "source", - structName: "foo", - transform: "snakecase", - }, - }, - { - file: "struct_format", - cfg: &config{ - add: []string{"gaum"}, - output: "source", - structName: "foo", - transform: "snakecase", - valueFormat: "field_name={field}", - }, - }, - { - file: "struct_format_existing", - cfg: &config{ - add: []string{"gaum"}, - output: "source", - structName: "foo", - transform: "snakecase", - valueFormat: "field_name={field}", - }, - }, - { - file: "struct_format_oldstyle", - cfg: &config{ - add: []string{"gaum"}, - output: "source", - structName: "foo", - transform: "snakecase", - valueFormat: "field_name=$field", - }, - }, - { - file: "struct_format_existing_oldstyle", - cfg: &config{ - add: []string{"gaum"}, - output: "source", - structName: "foo", - transform: "snakecase", - valueFormat: "field_name=$field", - }, - }, - { - file: "struct_remove", - cfg: &config{ - remove: []string{"json"}, - output: "source", - structName: "foo", - }, - }, - { - file: "struct_clear_tags", - cfg: &config{ - clear: true, - output: "source", - structName: "foo", - }, - }, - { - file: "struct_clear_options", - cfg: &config{ - clearOption: true, - output: "source", - structName: "foo", - }, - }, - { - file: "line_add", - cfg: &config{ - add: []string{"json"}, - output: "source", - line: "4", - transform: "snakecase", - }, - }, - { - file: "line_add_override", - cfg: &config{ - add: []string{"json"}, - output: "source", - line: "4,5", - transform: "snakecase", - override: true, - }, - }, - { - file: "line_add_override_column", - cfg: &config{ - add: []string{"json:MyBar:bar"}, - output: "source", - line: "4,4", - transform: "snakecase", - override: true, - }, - }, - { - file: "line_add_override_mixed_column_and_equal", - cfg: &config{ - add: []string{"json:MyBar:bar:foo=qux"}, - output: "source", - line: "4,4", - transform: "snakecase", - override: true, - }, - }, - { - file: "line_add_override_multi_equal", - cfg: &config{ - add: []string{"json:MyBar=bar=foo"}, - output: "source", - line: "4,4", - transform: "snakecase", - override: true, - }, - }, - { - file: "line_add_override_multi_column", - cfg: &config{ - add: []string{"json:MyBar:bar:foo"}, - output: "source", - line: "4,4", - transform: "snakecase", - override: true, - }, - }, - { - file: "line_add_no_override", - cfg: &config{ - add: []string{"json"}, - output: "source", - line: "4,5", - transform: "snakecase", - }, - }, - { - file: "line_add_outside", - cfg: &config{ - add: []string{"json"}, - output: "source", - line: "2,8", - transform: "snakecase", - }, - }, - { - file: "line_add_outside_partial_start", - cfg: &config{ - add: []string{"json"}, - output: "source", - line: "2,5", - transform: "snakecase", - }, - }, - { - file: "line_add_outside_partial_end", - cfg: &config{ - add: []string{"json"}, - output: "source", - line: "5,8", - transform: "snakecase", - }, - }, - { - file: "line_add_intersect_partial", - cfg: &config{ - add: []string{"json"}, - output: "source", - line: "5,11", - transform: "snakecase", - }, - }, - { - file: "line_add_comment", - cfg: &config{ - add: []string{"json"}, - output: "source", - line: "6,7", - transform: "snakecase", - }, - }, - { - file: "line_add_option", - cfg: &config{ - addOptions: []string{"json=omitempty"}, - output: "source", - line: "4,7", - }, - }, - { - file: "line_add_option_existing", - cfg: &config{ - addOptions: []string{"json=omitempty"}, - output: "source", - line: "6,8", - }, - }, - { - file: "line_add_multiple_option", - cfg: &config{ - addOptions: []string{"json=omitempty", "hcl=squash"}, - add: []string{"hcl"}, - output: "source", - line: "4,7", - transform: "snakecase", - }, - }, - { - file: "line_add_option_with_equal", - cfg: &config{ - addOptions: []string{"validate=max=32"}, - add: []string{"validate"}, - output: "source", - line: "4,7", - transform: "snakecase", - }, - }, - { - file: "line_remove", - cfg: &config{ - remove: []string{"json"}, - output: "source", - line: "5,7", - }, - }, - { - file: "line_remove_option", - cfg: &config{ - removeOptions: []string{"hcl=squash"}, - output: "source", - line: "4,8", - }, - }, - { - file: "line_remove_options", - cfg: &config{ - removeOptions: []string{"json=omitempty", "hcl=omitnested"}, - output: "source", - line: "4,7", - }, - }, - { - file: "line_remove_option_with_equal", - cfg: &config{ - removeOptions: []string{"validate=max=32"}, - output: "source", - line: "4,7", - }, - }, - { - file: "line_multiple_add", - cfg: &config{ - add: []string{"json"}, - output: "source", - line: "5,6", - transform: "camelcase", - }, - }, - { - file: "line_lispcase_add", - cfg: &config{ - add: []string{"json"}, - output: "source", - line: "4,6", - transform: "lispcase", - }, - }, - { - file: "line_camelcase_add", - cfg: &config{ - add: []string{"json"}, - output: "source", - line: "4,5", - transform: "camelcase", - }, - }, - { - file: "line_camelcase_add_embedded", - cfg: &config{ - add: []string{"json"}, - output: "source", - line: "4,6", - transform: "camelcase", - }, - }, - { - file: "line_value_add", - cfg: &config{ - add: []string{"json:foo"}, - output: "source", - line: "4,6", - }, - }, - { - file: "offset_add", - cfg: &config{ - add: []string{"json"}, - output: "source", - offset: 32, - transform: "snakecase", - }, - }, - { - file: "offset_add_composite", - cfg: &config{ - add: []string{"json"}, - output: "source", - offset: 40, - transform: "snakecase", - }, - }, - { - file: "offset_add_duplicate", - cfg: &config{ - add: []string{"json"}, - output: "source", - offset: 209, - transform: "snakecase", - }, - }, - { - file: "offset_add_literal_in", - cfg: &config{ - add: []string{"json"}, - output: "source", - offset: 46, - transform: "snakecase", - }, - }, - { - file: "offset_add_literal_out", - cfg: &config{ - add: []string{"json"}, - output: "source", - offset: 32, - transform: "snakecase", - }, - }, - { - file: "errors", - cfg: &config{ - add: []string{"json"}, - output: "source", - line: "4,7", - transform: "snakecase", - }, - }, - { - file: "line_pascalcase_add", - cfg: &config{ - add: []string{"json"}, - output: "source", - line: "4,5", - transform: "pascalcase", - }, - }, - { - file: "line_pascalcase_add_embedded", - cfg: &config{ - add: []string{"json"}, - output: "source", - line: "4,6", - transform: "pascalcase", - }, - }, - { - file: "not_formatted", - cfg: &config{ - add: []string{"json"}, - output: "source", - line: "3,4", - transform: "snakecase", - }, - }, - { - file: "skip_private", - cfg: &config{ - add: []string{"json"}, - output: "source", - structName: "foo", - transform: "snakecase", - skipUnexportedFields: true, - }, - }, - { - file: "skip_private_multiple_names", - cfg: &config{ - add: []string{"json"}, - output: "source", - structName: "foo", - transform: "snakecase", - skipUnexportedFields: true, - }, - }, - { - file: "skip_embedded", - cfg: &config{ - add: []string{"json"}, - output: "source", - structName: "StationCreated", - transform: "snakecase", - skipUnexportedFields: true, - }, - }, - { - file: "all_structs", - cfg: &config{ - add: []string{"json"}, - output: "source", - all: true, - transform: "snakecase", - }, - }, - { - file: "line_titlecase_add", - cfg: &config{ - add: []string{"json"}, - output: "source", - line: "4,6", - transform: "titlecase", - }, - }, - { - file: "line_titlecase_add_embedded", - cfg: &config{ - add: []string{"json"}, - output: "source", - line: "4,6", - transform: "titlecase", - }, - }, - { - file: "field_add", - cfg: &config{ - add: []string{"json"}, - output: "source", - structName: "foo", - fieldName: "bar", - transform: "snakecase", - }, - }, - { - file: "field_add_same_line", - cfg: &config{ - add: []string{"json"}, - output: "source", - structName: "foo", - fieldName: "qux", - transform: "snakecase", - }, - }, - { - file: "field_add_existing", - cfg: &config{ - add: []string{"json"}, - output: "source", - structName: "foo", - fieldName: "bar", - transform: "snakecase", - }, - }, - { - file: "field_clear_tags", - cfg: &config{ - clear: true, - output: "source", - structName: "foo", - fieldName: "bar", - }, - }, - { - file: "field_clear_options", - cfg: &config{ - clearOption: true, - output: "source", - structName: "foo", - fieldName: "bar", - }, - }, - { - file: "field_remove", - cfg: &config{ - remove: []string{"json"}, - output: "source", - structName: "foo", - fieldName: "bar", - }, - }, - { - file: "offset_anonymous_struct", - cfg: &config{ - add: []string{"json"}, - output: "source", - offset: 45, - transform: "camelcase", - }, - }, - { - file: "offset_star_struct", - cfg: &config{ - add: []string{"json"}, - output: "source", - offset: 35, - transform: "camelcase", - }, - }, - { - file: "offset_array_struct", - cfg: &config{ - add: []string{"json"}, - output: "source", - offset: 35, - transform: "camelcase", - }, - }, - } - - for _, ts := range test { - t.Run(ts.file, func(t *testing.T) { - ts.cfg.file = filepath.Join(fixtureDir, fmt.Sprintf("%s.input", ts.file)) - - node, err := ts.cfg.parse() - if err != nil { - t.Fatal(err) - } - - start, end, err := ts.cfg.findSelection(node) - if err != nil { - t.Fatal(err) - } - - rewrittenNode, err := ts.cfg.rewrite(node, start, end) - if err != nil { - if _, ok := err.(*rewriteErrors); !ok { - t.Fatal(err) - } - } - - out, err := ts.cfg.format(rewrittenNode, err) - if err != nil { - t.Fatal(err) - } - got := []byte(out) - - // update golden file if necessary - golden := filepath.Join(fixtureDir, fmt.Sprintf("%s.golden", ts.file)) - if *update { - err := ioutil.WriteFile(golden, got, 0644) - if err != nil { - t.Error(err) - } - return - } - - // get golden file - want, err := ioutil.ReadFile(golden) - if err != nil { - t.Fatal(err) - } - - var from []byte - if ts.cfg.modified != nil { - from, err = ioutil.ReadAll(ts.cfg.modified) - } else { - from, err = ioutil.ReadFile(ts.cfg.file) - } - if err != nil { - t.Fatal(err) - } - - // compare - if !bytes.Equal(got, want) { - t.Errorf("case %s\ngot:\n====\n\n%s\nwant:\n=====\n\n%s\nfrom:\n=====\n\n%s\n", - ts.file, got, want, from) - } - }) - } -} - -func TestJSON(t *testing.T) { - test := []struct { - cfg *config - file string - err error - }{ - { - file: "json_single", - cfg: &config{ - add: []string{"json"}, - line: "5", - }, - }, - { - file: "json_full", - cfg: &config{ - add: []string{"json"}, - line: "4,6", - }, - }, - { - file: "json_intersection", - cfg: &config{ - add: []string{"json"}, - line: "5,16", - }, - }, - { - // both small & end range larger than file - file: "json_single", - cfg: &config{ - add: []string{"json"}, - line: "30,32", // invalid selection - }, - err: errors.New("line selection is invalid"), - }, - { - // end range larger than file - file: "json_single", - cfg: &config{ - add: []string{"json"}, - line: "4,50", // invalid selection - }, - err: errors.New("line selection is invalid"), - }, - { - file: "json_errors", - cfg: &config{ - add: []string{"json"}, - line: "4,7", - }, - }, - { - file: "json_not_formatted", - cfg: &config{ - add: []string{"json"}, - line: "3,4", - }, - }, - { - file: "json_not_formatted_2", - cfg: &config{ - add: []string{"json"}, - line: "3,3", - }, - }, - { - file: "json_not_formatted_3", - cfg: &config{ - add: []string{"json"}, - offset: 23, - }, - }, - { - file: "json_not_formatted_4", - cfg: &config{ - add: []string{"json"}, - offset: 51, - }, - }, - { - file: "json_not_formatted_5", - cfg: &config{ - add: []string{"json"}, - offset: 29, - }, - }, - { - file: "json_not_formatted_6", - cfg: &config{ - add: []string{"json"}, - line: "2,54", - }, - }, - { - file: "json_all_structs", - cfg: &config{ - add: []string{"json"}, - all: true, - }, - }, - } - - for _, ts := range test { - t.Run(ts.file, func(t *testing.T) { - ts.cfg.file = filepath.Join(fixtureDir, fmt.Sprintf("%s.input", ts.file)) - // these are explicit and shouldn't be changed for this particular - // main test - ts.cfg.output = "json" - ts.cfg.transform = "camelcase" - - node, err := ts.cfg.parse() - if err != nil { - t.Fatal(err) - } - - start, end, err := ts.cfg.findSelection(node) - if err != nil { - t.Fatal(err) - } - - rewrittenNode, err := ts.cfg.rewrite(node, start, end) - if err != nil { - if _, ok := err.(*rewriteErrors); !ok { - t.Fatal(err) - } - } - - out, err := ts.cfg.format(rewrittenNode, err) - if !reflect.DeepEqual(err, ts.err) { - t.Logf("want: %v", ts.err) - t.Logf("got: %v", err) - t.Fatalf("unexpected error") - } - - if ts.err != nil { - return - } - - got := []byte(out) - - // update golden file if necessary - golden := filepath.Join(fixtureDir, fmt.Sprintf("%s.golden", ts.file)) - if *update { - err := ioutil.WriteFile(golden, got, 0644) - if err != nil { - t.Error(err) - } - return - } - - // get golden file - want, err := ioutil.ReadFile(golden) - if err != nil { - t.Fatal(err) - } - - from, err := ioutil.ReadFile(ts.cfg.file) - if err != nil { - t.Fatal(err) - } - - // compare - if !bytes.Equal(got, want) { - t.Errorf("case %s\ngot:\n====\n\n%s\nwant:\n=====\n\n%s\nfrom:\n=====\n\n%s\n", - ts.file, got, want, from) - } - }) - } -} - -func TestModifiedRewrite(t *testing.T) { - cfg := &config{ - add: []string{"json"}, - output: "source", - structName: "foo", - transform: "snakecase", - file: "struct_add_modified", - modified: strings.NewReader(`struct_add_modified -55 -package foo - -type foo struct { - bar string - t bool -} -`), - } - - node, err := cfg.parse() - if err != nil { - t.Fatal(err) - } - - start, end, err := cfg.findSelection(node) - if err != nil { - t.Fatal(err) - } - - rewrittenNode, err := cfg.rewrite(node, start, end) - if err != nil { - t.Fatal(err) - } - - got, err := cfg.format(rewrittenNode, err) - if err != nil { - t.Fatal(err) - } - - golden := filepath.Join(fixtureDir, "struct_add.golden") - want, err := ioutil.ReadFile(golden) - if err != nil { - t.Fatal(err) - } - - // compare - if !bytes.Equal([]byte(got), want) { - t.Errorf("got:\n====\n%s\nwant:\n====\n%s\n", got, want) - } -} - -func TestModifiedFileMissing(t *testing.T) { - cfg := &config{ - add: []string{"json"}, - output: "source", - structName: "foo", - transform: "snakecase", - file: "struct_add_modified", - modified: strings.NewReader(`file_that_doesnt_exist -55 -package foo - -type foo struct { - bar string - t bool -} -`), - } - - _, err := cfg.parse() - if err == nil { - t.Fatal("expected error") - } -} - -func TestParseLines(t *testing.T) { - var tests = []struct { - file string - }{ - {file: "line_directive_unix"}, - {file: "line_directive_windows"}, - } - - for _, ts := range tests { - ts := ts - - t.Run(ts.file, func(t *testing.T) { - filePath := filepath.Join(fixtureDir, fmt.Sprintf("%s.input", ts.file)) - - file, err := os.Open(filePath) - if err != nil { - t.Fatal(err) - } - defer file.Close() - - out, err := parseLines(file) - if err != nil { - t.Fatal(err) - } - - toBytes := func(lines []string) []byte { - var buf bytes.Buffer - for _, line := range lines { - buf.WriteString(line + "\n") - } - return buf.Bytes() - } - - got := toBytes(out) - - // update golden file if necessary - golden := filepath.Join(fixtureDir, fmt.Sprintf("%s.golden", ts.file)) - - if *update { - err := ioutil.WriteFile(golden, got, 0644) - if err != nil { - t.Error(err) - } - return - } - - // get golden file - want, err := ioutil.ReadFile(golden) - if err != nil { - t.Fatal(err) - } - - from, err := ioutil.ReadFile(filePath) - if err != nil { - t.Fatal(err) - } - - // compare - if !bytes.Equal(got, want) { - t.Errorf("case %s\ngot:\n====\n\n%s\nwant:\n=====\n\n%s\nfrom:\n=====\n\n%s\n", - ts.file, got, want, from) - } - - }) - } -} - func TestParseConfig(t *testing.T) { - // don't output help message during the test + // don't Output help message during the test flag.CommandLine.SetOutput(ioutil.Discard) // The flag.CommandLine.Parse() call fails if there are flags re-defined