diff --git a/impl.go b/impl.go index 9746cfe..fef12da 100644 --- a/impl.go +++ b/impl.go @@ -80,38 +80,123 @@ func parseType(in string) (Type, error) { // "Iface[a/b.T, c/d.U]" -> "Iface[b.T, d.U]" // "Iface[a/b.Other[c/d.T]]" -> "Iface[b.Other[d.T]]" // "Iface[*a/b.T]" -> "Iface[*b.T]" -func stripPaths(in string) string { - runes := []rune(in) - out := make([]rune, 0, len(runes)) - for len(runes) > 0 { - // Find extent of path-like segment - n := slices.IndexFunc(runes, isNonPathRune) - seg := runes - if n >= 0 { - seg = seg[:n] - } - if slash := lastIndex(seg, '/'); slash >= 0 { - seg = seg[slash+1:] - } +// For more examples, see the tests in impl_test.go +// +// Because of the staggered parsing, the handling of quoted paths is done in +// a way that supports seeing the start and end quote in separate loop +// iterations. +// +// Algorithm Example: +// Given: "github.com/foo".Iface +// +// 1. First iteration +// a. Grab all path characters (none, because " is first) +// b. Grab all non-path characters (") +// c. Strip quotes (remove only the one) +// 2. Second iteration +// a. Grab all path characters (github.com/foo) +// b. Grab all non-path characters (") +// c. Strip second quote +// 3. etc +func stripPaths(in string) (string, error) { + remain := []rune(in) + out := make([]rune, 0, len(remain)) + quotesRemoved := 0 + + for len(remain) > 0 { + var seg []rune + var more bool + + seg, remain, more = getPathSeg(remain) out = append(out, seg...) - if n == -1 { + if !more { break } - runes = runes[n:] - // Copy non-path runes verbatim - n = slices.IndexFunc(runes, isPathRune) - seg = runes - if n >= 0 { - seg = seg[:n] + seg, remain, more = getNonPathSeg(remain, "esRemoved) + // Check for remaining quote in segment. This is to handle + // double quotes + if checkForQuote(seg) { + return "", fmt.Errorf("double quotes") } out = append(out, seg...) - if n == -1 { + if !more { break } - runes = runes[n:] } - return string(out) + + // We want balanced quotes for our paths + if quotesRemoved % 2 != 0 { + return "", fmt.Errorf("unbalanced quotes") + } + + return string(out), nil +} + +func getNonPathSeg(runes []rune, quotesRemoved *int) (seg []rune, remain []rune, more bool) { + // Get index of next path character + n := slices.IndexFunc(runes, isPathRune) + // Copy all characters before the path character + seg = runes + if n >= 0 { + seg = seg[:n] + } + // Trim a quote from the segment + lenPreTrim := len(seg) + seg = trimPathSeg(seg) + // If a quote was removed, increment the number of quotes removed + // This is for checking that the quotations are balanced + *quotesRemoved += lenPreTrim - len(seg) + // If there are no path like characters, we are done + if n == -1 { + return seg, []rune{}, false + } + remain = runes[n:] + return seg, remain, true +} + +func getPathSeg(runes []rune) (seg []rune, remain []rune, more bool) { + // Find first index of a non-path character + n := slices.IndexFunc(runes, isNonPathRune) + // Get characters up to the non-path character + seg = runes + if n >= 0 { + seg = seg[:n] + } + // If there is a path separator, get the segment at + // the end of the path + if slash := lastIndex(seg, '/'); slash >= 0 { + seg = seg[slash+1:] + } + // if there is no non-path like characters, we are done + if n == -1 { + return seg, []rune{}, false + } + remain = runes[n:] + return seg, remain, true +} + +func checkForQuote(p []rune) bool { + if len(p) == 0 { + return false + } + return p[0] == '"' || p[len(p)-1] == '"' +} + +func trimPathSeg(p []rune) []rune { + if len(p) == 0 { + return p + } + + if p[0] == '"' { + return p[1:] + } + + if p[len(p)-1] == '"' { + return p[:len(p)-1] + } + + return p } // lastIndex returns the index of the last occurrence of v in s, or -1 if not present. @@ -175,8 +260,11 @@ func findInterface(input string, srcDir string) (path string, iface Type, err er if dot <= slash { return "", Type{}, fmt.Errorf("invalid interface name: %s", input) } - path = baseInput[:dot] - id := stripPaths(input[dot+1:]) + path = strings.Trim(baseInput[:dot], "\"") + id, err := stripPaths(input[dot+1:]) + if err != nil { + return "", Type{}, err + } iface, err = parseType(id) if err != nil { return "", Type{}, err diff --git a/impl_test.go b/impl_test.go index 81d4f9e..3d16e3c 100644 --- a/impl_test.go +++ b/impl_test.go @@ -34,6 +34,7 @@ func TestFindInterface(t *testing.T) { {input: "a/b/c/pkg", wantErr: true}, {input: "a/b/c/pkg.", wantErr: true}, {input: "a/b/c/pkg.Typ", path: "a/b/c/pkg", typ: Type{Name: "Typ"}}, + {input: `"a/b/c/pkg".Typ`, path: "a/b/c/pkg", typ: Type{Name: "Typ"}}, {input: "gopkg.in/yaml.v2.Unmarshaler", path: "gopkg.in/yaml.v2", typ: Type{Name: "Unmarshaler"}}, {input: "github.com/josharian/impl/testdata.GenericInterface1[string]", path: "github.com/josharian/impl/testdata", typ: Type{Name: "GenericInterface1", Params: []string{"string"}}}, {input: "github.com/josharian/impl/testdata.GenericInterface1[*string]", path: "github.com/josharian/impl/testdata", typ: Type{Name: "GenericInterface1", Params: []string{"*string"}}}, @@ -55,6 +56,17 @@ func TestFindInterface(t *testing.T) { {input: "github.com/josharian/impl/testdata.GenericInterface1[*github.com/josharian/impl/testdata.Struct5]", path: "github.com/josharian/impl/testdata", typ: Type{Name: "GenericInterface1", Params: []string{"*testdata.Struct5"}}}, // Hyphenated package path (hyphens in path segments, not in final package name) {input: "github.com/go-chi/chi.Router[github.com/some-org/pkg.SomeType]", path: "github.com/go-chi/chi", typ: Type{Name: "Router", Params: []string{"pkg.SomeType"}}}, + + // Quoted path edge cases - unbalanced/double quotes + //// missing closing quote + //{input: `"a/b/c/pkg.Typ`, wantErr: true}, + //// missing opening quote + //{input: `a/b/c/pkg".Typ`, wantErr: true}, + //// quote after type name + //{input: `"a/b/c/pkg.Typ"`, wantErr: true}, + //// double quotes + //{input: `""a/b/c/pkg"".Typ`, wantErr: true}, + //{input: `"github.com/josharian/impl/testdata".Interface1`, path: "github.com/josharian/impl/testdata", typ: Type{Name: "Interface1"}}, } for _, tt := range cases { @@ -693,35 +705,35 @@ func TestStubGenerationForImplemented(t *testing.T) { want string }{ { - desc: "without implemeted methods", + desc: "without implemented methods", iface: "github.com/josharian/impl/testdata.Interface3", recv: "r *Implemented", recvPkg: "testdata", want: testdata.Interface4Output, }, { - desc: "without implemeted methods with trailing space", + desc: "without implemented methods with trailing space", iface: "github.com/josharian/impl/testdata.Interface3", recv: "r *Implemented ", recvPkg: "testdata", want: testdata.Interface4Output, }, { - desc: "without implemeted methods, with generic receiver", + desc: "without implemented methods, with generic receiver", iface: "github.com/josharian/impl/testdata.Interface3", recv: "r *ImplementedGeneric[Type1]", recvPkg: "testdata", want: testdata.Interface4GenericOutput, }, { - desc: "without implemeted methods, with generic receiver with multiple params", + desc: "without implemented methods, with generic receiver with multiple params", iface: "github.com/josharian/impl/testdata.Interface3", recv: "r *ImplementedGenericMultipleParams[Type1, Type2]", recvPkg: "testdata", want: testdata.Interface4GenericMultipleParamsOutput, }, { - desc: "without implemeted methods and receiver variable", + desc: "without implemented methods and receiver variable", iface: "github.com/josharian/impl/testdata.Interface3", recv: "*Implemented", recvPkg: "testdata", @@ -911,11 +923,22 @@ func TestStripPaths(t *testing.T) { desc string input string want string + wantErr bool }{ {desc: "no path", input: "Iface", want: "Iface"}, {desc: "simple path", input: "a/b.T", want: "b.T"}, + {desc: "simple quoted path", input: `"a/b".T`, want: "b.T"}, + {desc: "simple unbalacned quote path", input: "\"a/b.T", wantErr: true}, + {desc: "simple unbalacned quote path 2", input: "a/b\".T", wantErr: true}, + {desc: "simple double quote path", input: "\"\"a/b\"\".T", wantErr: true}, + {desc: "simple unbalanced double quote path", input: "a/b\"\".T", wantErr: true}, + {desc: "simple unbalanced double quote path 2", input: "\"\"a/b.T", wantErr: true}, {desc: "deep path", input: "github.com/foo/bar.T", want: "bar.T"}, + {desc: "deep quoted path", input: `"github.com/foo/bar".T`, want: "bar.T"}, {desc: "generic with path param", input: "Iface[github.com/foo/bar.T]", want: "Iface[bar.T]"}, + {desc: "generic and interface with path param", input: "github.com/foo.Iface[github.com/foo/bar.T]", want: "foo.Iface[bar.T]"}, + {desc: "generic and interface with quoted path param", input: `"github.com/foo".Iface["github.com/foo/bar".T]`, want: "foo.Iface[bar.T]"}, + {desc: "generic with quoted path param", input: `Iface["github.com/foo/bar".T]`, want: "Iface[bar.T]"}, {desc: "multiple path params", input: "Iface[a/b.T, c/d.U]", want: "Iface[b.T, d.U]"}, {desc: "nested generic with paths", input: "Iface[a/b.Other[c/d.T]]", want: "Iface[b.Other[d.T]]"}, {desc: "pointer to path type", input: "Iface[*a/b.T]", want: "Iface[*b.T]"}, @@ -962,7 +985,13 @@ func TestStripPaths(t *testing.T) { for _, tt := range cases { t.Run(tt.desc, func(t *testing.T) { t.Parallel() - got := stripPaths(tt.input) + got, err := stripPaths(tt.input) + if err == nil && tt.wantErr { + t.Errorf("stripPaths(%q) = %q, want error", tt.input, got) + } + if err != nil && !tt.wantErr { + t.Errorf("stripPaths(%q) = got error %v, want %q", tt.input, err, tt.want) + } if got != tt.want { t.Errorf("stripPaths(%q) = %q, want %q", tt.input, got, tt.want) }