├── scripts ├── go-diff.sh └── go-diff.gs ├── ACKNOWLEDGEMENTS ├── .gitignore ├── godiff.go ├── LICENSE ├── README.md ├── cmd ├── godiff_test.go └── godiff.go └── tm ├── tm_test.go └── tm.go /scripts/go-diff.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | go-diff $2 $5 -------------------------------------------------------------------------------- /ACKNOWLEDGEMENTS: -------------------------------------------------------------------------------- 1 | Dmitri Shuralyov (@shurcooL) for cleaning up of the code. 2 | -------------------------------------------------------------------------------- /scripts/go-diff.gs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env gosl 2 | # gosl: https://github.com/daviddengcn/gosl 3 | 4 | import godiff "github.com/daviddengcn/go-diff/cmd" 5 | 6 | if len(Args) < 7 { 7 | Fatalf("go-diff.gs is supposed to be called from Git") 8 | } 9 | 10 | godiff.Exec(Args[2], Args[5], godiff.Options{}) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | -------------------------------------------------------------------------------- /godiff.go: -------------------------------------------------------------------------------- 1 | /* 2 | go-diff is a tool checking semantic difference between source files. 3 | 4 | Currently supported language: 5 | 6 | - Go (fully) 7 | 8 | If the language is not supported or parsing is failed for either file, 9 | a line-to-line comparing is imposed. 10 | */ 11 | package main 12 | 13 | import ( 14 | "flag" 15 | "os" 16 | 17 | "github.com/daviddengcn/go-diff/cmd" 18 | "github.com/golangplus/fmt" 19 | ) 20 | 21 | func usage() { 22 | fmtp.Eprintfln("usage: go-diff [options] org-filename new-filename") 23 | flag.PrintDefaults() 24 | os.Exit(2) 25 | } 26 | 27 | func main() { 28 | var options godiff.Options 29 | 30 | flag.BoolVar(&options.NoColor, "no-color", false, "turn off the colors") 31 | 32 | flag.Usage = usage 33 | flag.Parse() 34 | 35 | if flag.NArg() < 2 { 36 | usage() 37 | return 38 | } // if 39 | orgFn := flag.Arg(0) 40 | newFn := flag.Arg(1) 41 | 42 | godiff.Exec(orgFn, newFn, options) 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Yi DENG 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | The views and conclusions contained in the software and documentation are those 25 | of the authors and should not be interpreted as representing official policies, 26 | either expressed or implied, of the FreeBSD Project. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-diff [![GoSearch](http://go-search.org/badge?id=github.com%2Fdaviddengcn%2Fgo-diff)](http://go-search.org/view?id=github.com%2Fdaviddengcn%2Fgo-diff) 2 | ======= 3 | 4 | A diff tool for [Go languange](http://golang.org/). It shows the semantic differences between two Go source files. 5 | 6 | Ignored Difference 7 | ------------------ 8 | 1. Order of import statements 9 | 1. Order of definitions of global type/const/var/func 10 | 1. Whether more than one parameters or global variables are declared in one line. e.g. var a, b int = 1, 2 is equivalent to var a int = 1; var b int = 2. (NOTE parallel assignments are not normalized) 11 | 1. All comments. 12 | 1. Code formats. e.g. some useless new lines. 13 | 14 | Other Features 15 | -------------- 16 | 1. Smart matching algorithm on go ast tree. 17 | 1. If a function is deleted or added as a whole, only one-line message is shown (starting by === or ###) 18 | 1. Easily see which function or type, etc. the difference is in. 19 | 1. Import/const/var/func diffrences are shown in order, independent of the lines' order in the source. 20 | 2. Token based line-line difference presentation. 21 | 22 | Installation 23 | ------------ 24 | ```bash 25 | $ go get -u github.com/daviddengcn/go-diff 26 | $ go install github.com/daviddengcn/go-diff 27 | $ go-diff 28 | ``` 29 | (Make sure $GO_PATH/bin is in system's $PATH) 30 | 31 | Used as git diff 32 | 33 | 1. Link `scripts/go-diff.gs` (you need install [gosl](http://github.com/daviddengcn/gosl)) or `scripts/go-diff.sh` to a folder in `PATH` 34 | 35 | 1. Set Git external diff driver (change `go-diff.gs` to `go-diff.sh` accordingly if necessary) 36 | 37 | ```bash 38 | $ git config [--global] diff.external go-diff.gs 39 | $ git diff 40 | ``` 41 | 42 | License 43 | ------- 44 | BSD license 45 | -------------------------------------------------------------------------------- /cmd/godiff_test.go: -------------------------------------------------------------------------------- 1 | package godiff 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/golangplus/bytes" 8 | "github.com/golangplus/testing/assert" 9 | ) 10 | 11 | func TestNodeToLines_Literal(t *testing.T) { 12 | info, err := parse("", ` 13 | package main 14 | 15 | func main() { 16 | a := Da { 17 | A: 10, 18 | B: 20, 19 | } 20 | } 21 | 22 | `) 23 | if !assert.NoError(t, err) { 24 | return 25 | } 26 | 27 | lines := info.funcs.sourceLines("") 28 | assert.StringEqual(t, "lines", lines, strings.Split( 29 | `func main() { 30 | a := Da{ 31 | A: 10, 32 | B: 20, 33 | } 34 | }`, "\n")) 35 | } 36 | 37 | func TestDiffLines_1(t *testing.T) { 38 | var buf bytesp.Slice 39 | gOut = &buf 40 | 41 | src := strings.Split(`This a line with the word abc different only 42 | This a line with the word def different only 43 | This a line with the word ghi different only 44 | This a line with the word jkl different only`, "\n") 45 | dst := strings.Split(`This a line with the word abc different only 46 | This a line with the word ghi different only 47 | This a line with the word jkl different only 48 | This a line with the word def different only`, "\n") 49 | diffLines(src, dst, "%s") 50 | 51 | assert.Equal(t, "diff", string(buf), ` This a line with the word abc different only 52 | --- This a line with the word def different only 53 | This a line with the word ghi different only 54 | This a line with the word jkl different only 55 | +++ This a line with the word def different only 56 | `) 57 | } 58 | 59 | func TestDiffLines_2(t *testing.T) { 60 | var buf bytesp.Slice 61 | gOut = &buf 62 | 63 | src := strings.Split(`abc 64 | { 65 | hello 66 | } 67 | defg`, "\n") 68 | dst := strings.Split(`abc 69 | { 70 | gogogo 71 | } 72 | { 73 | hello 74 | } 75 | defg`, "\n") 76 | diffLines(src, dst, "%s") 77 | 78 | t.Logf("Diff: %s", string(buf)) 79 | 80 | assert.StringEqual(t, "diff", strings.Split(string(buf), "\n"), 81 | strings.Split(` abc 82 | +++ { 83 | +++ gogogo 84 | +++ } 85 | { 86 | ... (2 lines) 87 | defg 88 | `, "\n")) 89 | } 90 | -------------------------------------------------------------------------------- /tm/tm_test.go: -------------------------------------------------------------------------------- 1 | package tm 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/golangplus/testing/assert" 8 | ) 9 | 10 | func TestMatchTokens1(t *testing.T) { 11 | delT, insT := LineToTokens("{abc{def}ghi}"), LineToTokens("{def}") 12 | assert.StringEqual(t, "delT", delT, "[{ abc { def } ghi }]") 13 | assert.StringEqual(t, "insT", insT, "[{ def }]") 14 | 15 | matA, matB := MatchTokens(delT, insT) 16 | assert.StringEqual(t, "matA", matA, "[-1 -1 0 1 2 -1 -1]") 17 | assert.StringEqual(t, "matB", matB, "[2 3 4]") 18 | } 19 | 20 | func TestMatchTokens2(t *testing.T) { 21 | delT, insT := LineToTokens("(a.b())"), LineToTokens("a.b()") 22 | assert.StringEqual(t, "delT", delT, "[( a . b ( ) )]") 23 | assert.StringEqual(t, "insT", insT, "[a . b ( )]") 24 | matA, matB := MatchTokens(delT, insT) 25 | assert.StringEqual(t, "matA", matA, "[-1 0 1 2 3 4 -1]") 26 | assert.StringEqual(t, "matB", matB, "[1 2 3 4 5]") 27 | 28 | delT, insT = LineToTokens("(a.b(), c)"), LineToTokens("(a.b, c)") 29 | assert.StringEqual(t, "delT", delT, "[( a . b ( ) , c )]") 30 | assert.StringEqual(t, "insT", insT, "[( a . b , c )]") 31 | matA, matB = MatchTokens(delT, insT) 32 | assert.StringEqual(t, "matA", matA, "[0 1 2 3 -1 -1 4 -1 6 7]") 33 | assert.StringEqual(t, "matB", matB, "[0 1 2 3 6 -1 8 9]") 34 | 35 | delT, insT = LineToTokens("[a.b[]]"), LineToTokens("[a.b]") 36 | assert.StringEqual(t, "delT", delT, "[[ a . b [ ] ]]") 37 | assert.StringEqual(t, "insT", insT, "[[ a . b ]]") 38 | matA, matB = MatchTokens(delT, insT) 39 | assert.StringEqual(t, "matA", matA, "[0 1 2 3 -1 -1 4]") 40 | assert.StringEqual(t, "matB", matB, "[0 1 2 3 6]") 41 | 42 | delT, insT = LineToTokens("(), (abc)"), LineToTokens("(abc)") 43 | assert.StringEqual(t, "delT", delT, "[( ) , ( abc )]") 44 | assert.StringEqual(t, "insT", insT, "[( abc )]") 45 | matA, matB = MatchTokens(delT, insT) 46 | assert.StringEqual(t, "matA", matA, "[-1 -1 -1 -1 0 1 2]") 47 | assert.StringEqual(t, "matB", matB, "[4 5 6]") 48 | 49 | delT, insT = LineToTokens(`"", "abc"`), LineToTokens(`"abc"`) 50 | assert.StringEqual(t, "delT", delT, `[" " , " abc "]`) 51 | assert.StringEqual(t, "insT", insT, `[" abc "]`) 52 | matA, matB = MatchTokens(delT, insT) 53 | assert.StringEqual(t, "matA", matA, "[-1 -1 -1 -1 0 1 2]") 54 | assert.StringEqual(t, "matB", matB, "[4 5 6]") 55 | 56 | delT, insT = LineToTokens(`cmd:=exec.Command("go", "abc")`), LineToTokens(`cmd:=villa.Path("go").Command("abc")`) 57 | assert.StringEqual(t, "delT", delT, `[cmd : = exec . Command ( " go " , " abc " )]`) 58 | assert.StringEqual(t, "insT", insT, `[cmd : = villa . Path ( " go " ) . Command ( " abc " )]`) 59 | matA, matB = MatchTokens(delT, insT) 60 | assert.StringEqual(t, "matA", matA, "[0 1 2 -1 11 12 13 -1 -1 -1 -1 -1 14 15 16 17]") 61 | assert.StringEqual(t, "matB", matB, "[0 1 2 -1 -1 -1 -1 -1 -1 -1 -1 4 5 6 12 13 14 15]") 62 | 63 | delT, insT = LineToTokens("Rename(m.exeFile(gsp))"), LineToTokens("Rename(exeFile)") 64 | assert.StringEqual(t, "delT", delT, `[Rename ( m . exe File ( gsp ) )]`) 65 | assert.StringEqual(t, "insT", insT, `[Rename ( exe File )]`) 66 | matA, matB = MatchTokens(delT, insT) 67 | assert.StringEqual(t, "matA", matA, "[0 1 -1 -1 2 3 -1 -1 -1 4]") 68 | assert.StringEqual(t, "matB", matB, "[0 1 4 5 9]") 69 | 70 | delT, insT = LineToTokens("Rename(exeFile)"), LineToTokens("Rename(m.exeFile(gsp))") 71 | assert.StringEqual(t, "delT", delT, `[Rename ( exe File )]`) 72 | assert.StringEqual(t, "insT", insT, `[Rename ( m . exe File ( gsp ) )]`) 73 | matA, matB = MatchTokens(delT, insT) 74 | assert.StringEqual(t, "matA", matA, "[0 1 4 5 9]") 75 | assert.StringEqual(t, "matB", matB, "[0 1 -1 -1 2 3 -1 -1 -1 4]") 76 | 77 | delT, insT = LineToTokens("Rename[m.exeFile[gsp]]"), LineToTokens("Rename[exeFile]") 78 | assert.StringEqual(t, "delT", delT, `[Rename [ m . exe File [ gsp ] ]]`) 79 | assert.StringEqual(t, "insT", insT, `[Rename [ exe File ]]`) 80 | matA, matB = MatchTokens(delT, insT) 81 | assert.StringEqual(t, "matA", matA, "[0 1 -1 -1 2 3 -1 -1 -1 4]") 82 | assert.StringEqual(t, "matB", matB, "[0 1 4 5 9]") 83 | } 84 | 85 | func TestMatchTokens3(t *testing.T) { 86 | MatchTokens(LineToTokens("[({gsp}}))]]"), LineToTokens("])}")) 87 | } 88 | 89 | func TestCalcDiffOfSourceLine(t *testing.T) { 90 | diff := CalcDiffOfSourceLine("return &monitor{", "m := &monitor{", 300) 91 | fmt.Println("diff", diff) 92 | 93 | diff = CalcDiffOfSourceLine("if (delO[iA] == 0) == (insO[iB] == 0) {", "if delL[iA] {", 1000) 94 | fmt.Println("diff", diff) 95 | diff = CalcDiffOfSourceLine("if (delO[iA] == 0) == (insO[iB] == 0) {", "c += diffAt(delT, iA - 1, insT, iB - 1)", 1000) 96 | fmt.Println("diff", diff) 97 | } 98 | -------------------------------------------------------------------------------- /tm/tm.go: -------------------------------------------------------------------------------- 1 | package tm 2 | 3 | import ( 4 | "github.com/daviddengcn/go-algs/ed" 5 | "github.com/daviddengcn/go-villa" 6 | "github.com/golangplus/math" 7 | ) 8 | 9 | const ( 10 | rune_SINGLE = iota 11 | rune_NUM 12 | rune_CAPITAL 13 | rune_LOWER 14 | ) 15 | 16 | func newToken(last, cur int) bool { 17 | if last == rune_SINGLE || cur == rune_SINGLE { 18 | return true 19 | } // if 20 | 21 | if last == cur { 22 | return false 23 | } // if 24 | 25 | if last == rune_NUM || cur == rune_NUM { 26 | return true 27 | } // if 28 | 29 | if last == rune_LOWER && cur == rune_CAPITAL { 30 | return true 31 | } // if 32 | 33 | return false 34 | } 35 | 36 | func runeType(r rune) int { 37 | switch { 38 | case r >= '0' && r <= '9': 39 | return rune_NUM 40 | 41 | case r >= 'A' && r <= 'Z': 42 | return rune_CAPITAL 43 | 44 | case r >= 'a' && r <= 'z': 45 | return rune_LOWER 46 | } // if 47 | 48 | return rune_SINGLE 49 | } 50 | 51 | func LineToTokens(line string) (tokens []string) { 52 | lastTp := rune_SINGLE 53 | for _, c := range line { 54 | tp := runeType(c) 55 | if newToken(lastTp, tp) { 56 | tokens = append(tokens, "") 57 | } 58 | tokens[len(tokens)-1] = tokens[len(tokens)-1] + string(c) 59 | 60 | lastTp = tp 61 | } 62 | 63 | return tokens 64 | } 65 | 66 | func diffAt(a []string, iA int, b []string, iB int) int { 67 | if iA < 0 { 68 | if iB < 0 { 69 | return 0 70 | } else { 71 | return 1 72 | } 73 | } 74 | if iB < 0 { 75 | return 1 76 | } 77 | 78 | if iA >= len(a) { 79 | if iB >= len(b) { 80 | return 0 81 | } else { 82 | return 1 83 | } 84 | } 85 | if iB >= len(b) { 86 | return 1 87 | } 88 | 89 | if a[iA] == b[iB] { 90 | return 0 91 | } 92 | 93 | return 2 94 | } 95 | 96 | func nearChecks(a []string) (l, r []bool) { 97 | l, r = make([]bool, len(a)), make([]bool, len(a)) 98 | 99 | const ( 100 | normal = iota 101 | doubleQuoted 102 | singleQuoted 103 | ) 104 | 105 | status := normal 106 | escaped := false 107 | for i, el := range a { 108 | l[i], r[i] = true, true 109 | 110 | switch status { 111 | case normal: 112 | switch el { 113 | case `"`: 114 | status = doubleQuoted 115 | l[i] = false 116 | case "'": 117 | status = singleQuoted 118 | l[i] = false 119 | case ",", ")": 120 | r[i] = false 121 | } 122 | case doubleQuoted: 123 | switch el { 124 | case `\`: 125 | if !escaped { 126 | escaped = true 127 | continue 128 | } 129 | case `"`: 130 | if !escaped { 131 | status = normal 132 | r[i] = false 133 | } 134 | } 135 | escaped = false 136 | case singleQuoted: 137 | switch el { 138 | case `\`: 139 | if !escaped { 140 | escaped = true 141 | continue 142 | } 143 | case "'": 144 | if !escaped { 145 | status = normal 146 | r[i] = false 147 | } 148 | } 149 | escaped = false 150 | } // switch status 151 | } 152 | 153 | return l, r 154 | } 155 | 156 | // if tks[i] and tks[j] are pairs, pairs[i], pairs[j] = j, i 157 | func findPairs(tks []string) (pairs []int) { 158 | pairs = make([]int, len(tks)) 159 | var s0, s1, s2 villa.IntSlice 160 | for i, tk := range tks { 161 | pairs[i] = -1 162 | 163 | switch tk { 164 | case "(": 165 | s0.Add(i) 166 | case ")": 167 | if len(s0) > 0 { 168 | j := s0.Pop() 169 | pairs[i], pairs[j] = j, i 170 | } 171 | 172 | case "[": 173 | s1.Add(i) 174 | case "]": 175 | if len(s1) > 0 { 176 | j := s1.Pop() 177 | pairs[i], pairs[j] = j, i 178 | } // if 179 | 180 | case "{": 181 | s2.Add(i) 182 | case "}": 183 | if len(s2) > 0 { 184 | j := s2.Pop() 185 | pairs[i], pairs[j] = j, i 186 | } // if 187 | } 188 | } 189 | 190 | return pairs 191 | } 192 | 193 | func noMatchBetween(mat []int, p1, p2 int) bool { 194 | if p2 < p1 { 195 | p1, p2 = p2, p1 196 | } 197 | for i := p1 + 1; i < p2; i++ { 198 | if mat[i] >= 0 { 199 | return false 200 | } 201 | } 202 | 203 | return true 204 | } 205 | 206 | /* 207 | if one of the pairs is match, and the other is not. Some adjustment can be performed. 208 | */ 209 | func alignPairs(matA, matB, pairA, pairB []int) { 210 | for { 211 | changed := false 212 | /* 213 | A i <---> j m 214 | ^ 7 215 | | / 216 | v L 217 | B k <---> l 218 | */ 219 | for i := range matA { 220 | j, k := pairA[i], matA[i] 221 | if j >= 0 && k >= 0 && matA[j] < 0 { 222 | l := pairB[k] 223 | if l >= 0 && matB[l] >= 0 { 224 | m := matB[l] 225 | if noMatchBetween(matA, j, m) { 226 | matA[j], matA[m], matB[l] = l, -1, j 227 | changed = true 228 | } 229 | } 230 | } 231 | } 232 | 233 | /* 234 | B i <---> j m 235 | ^ 7 236 | | / 237 | v L 238 | A k <---> l 239 | */ 240 | for i := range matB { 241 | j, k := pairB[i], matB[i] 242 | if j >= 0 && k >= 0 && matB[j] < 0 { 243 | l := pairA[k] 244 | if l >= 0 && matA[l] >= 0 { 245 | m := matA[l] 246 | if noMatchBetween(matB, j, m) { 247 | matB[j], matB[m], matA[l] = l, -1, j 248 | changed = true 249 | } 250 | } 251 | } 252 | } 253 | 254 | if !changed { 255 | break 256 | } 257 | } 258 | } 259 | 260 | func MatchTokens(delT, insT []string) (matA, matB []int) { 261 | delL, delR := nearChecks(delT) 262 | 263 | _, matA, matB = ed.EditDistanceFFull(len(delT), len(insT), func(iA, iB int) int { 264 | if delT[iA] == insT[iB] { 265 | c := 0 266 | if delL[iA] { 267 | c += diffAt(delT, iA-1, insT, iB-1) 268 | } 269 | if delR[iA] { 270 | c += diffAt(delT, iA+1, insT, iB+1) 271 | } 272 | return c 273 | } // if 274 | return len(delT[iA]) + len(insT[iB]) + 5 275 | }, func(iA int) int { 276 | if delT[iA] == " " { 277 | return 0 278 | } // if 279 | 280 | return len(delT[iA]) + 2 281 | }, func(iB int) int { 282 | if insT[iB] == " " { 283 | return 0 284 | } // if 285 | 286 | return len(insT[iB]) + 2 287 | }) 288 | 289 | delP, insP := findPairs(delT), findPairs(insT) 290 | alignPairs(matA, matB, delP, insP) 291 | 292 | return matA, matB 293 | } 294 | 295 | func DiffOfStrings(a, b string, mx int) int { 296 | if a == b { 297 | return 0 298 | } // if 299 | return ed.String(a, b) * mx / mathp.MaxI(len(a), len(b)) 300 | } 301 | 302 | var key_WORDS villa.StrSet = villa.NewStrSet( 303 | "if", "for", "return", "switch", "case", "select", "go") 304 | 305 | func isKeywords(a []string) (res []bool) { 306 | res = make([]bool, len(a)) 307 | for i, w := range a { 308 | res[i] = key_WORDS.In(w) 309 | } 310 | 311 | return res 312 | } 313 | 314 | func CalcDiffOfSourceLine(a, b string, mx int) int { 315 | if a == b { 316 | return 0 317 | } // if 318 | 319 | delT, insT := LineToTokens(a), LineToTokens(b) 320 | delK, insK := isKeywords(delT), isKeywords(insT) 321 | 322 | diff := ed.EditDistanceF(len(delT), len(insT), func(iA, iB int) int { 323 | if delT[iA] == insT[iB] { 324 | return 0 325 | } // if 326 | return 50 327 | }, func(iA int) int { 328 | if delK[iA] { 329 | return 2 330 | } 331 | return 1 332 | }, func(iB int) int { 333 | if insK[iB] { 334 | return 2 335 | } 336 | 337 | return 1 338 | }) 339 | 340 | return diff * mx / (len(delT) + len(insT)) 341 | } 342 | -------------------------------------------------------------------------------- /cmd/godiff.go: -------------------------------------------------------------------------------- 1 | /* 2 | go-diff is a tool checking semantic difference between source files. 3 | 4 | Currently supported language: 5 | 6 | - Go (fully) 7 | 8 | If the language is not supported or parsing is failed for either file, 9 | a line-to-line comparing is imposed. 10 | */ 11 | package godiff 12 | 13 | import ( 14 | "bytes" 15 | "fmt" 16 | "go/ast" 17 | "go/parser" 18 | "go/printer" 19 | "go/token" 20 | "io" 21 | "math" 22 | "os" 23 | "sort" 24 | "strings" 25 | 26 | "github.com/daviddengcn/go-algs/ed" 27 | "github.com/daviddengcn/go-colortext" 28 | "github.com/daviddengcn/go-diff/tm" 29 | "github.com/daviddengcn/go-villa" 30 | "github.com/golangplus/fmt" 31 | "github.com/golangplus/math" 32 | ) 33 | 34 | func cat(a, sep, b string) string { 35 | if len(a) > 0 && len(b) > 0 { 36 | return a + sep + b 37 | } 38 | 39 | return a + b 40 | } 41 | 42 | func changeColor(fg ct.Color, fgBright bool, bg ct.Color, bgBright bool) { 43 | if gOptions.NoColor { 44 | return 45 | } 46 | 47 | ct.ChangeColor(fg, fgBright, bg, bgBright) 48 | } 49 | 50 | func resetColor() { 51 | if gOptions.NoColor { 52 | return 53 | } 54 | 55 | ct.ResetColor() 56 | } 57 | 58 | func greedyMatch(lenA, lenB int, diffF func(iA, iB int) int, delCost, insCost func(int) int) (diffMat villa.IntMatrix, cost int, matA, matB []int) { 59 | matA, matB = make([]int, lenA), make([]int, lenB) 60 | villa.IntSlice(matA).Fill(0, lenA, -1) 61 | villa.IntSlice(matB).Fill(0, lenB, -1) 62 | 63 | diffMat = villa.NewIntMatrix(lenA, lenB) 64 | 65 | for iA := 0; iA < lenA; iA++ { 66 | if matA[iA] >= 0 { 67 | continue 68 | } 69 | for iB := 0; iB < lenB; iB++ { 70 | if matB[iB] >= 0 { 71 | continue 72 | } 73 | 74 | d := diffF(iA, iB) 75 | diffMat[iA][iB] = d 76 | 77 | if d == 0 { 78 | matA[iA], matB[iB] = iB, iA 79 | break 80 | } 81 | } 82 | } 83 | 84 | mat := diffMat.Clone() 85 | // mx is a number greater or equal to all mat elements (need not be the exact maximum) 86 | mx := 0 87 | for iA := range mat { 88 | if matA[iA] >= 0 { 89 | continue 90 | } // if 91 | for iB := 0; iB < lenB; iB++ { 92 | if matB[iB] >= 0 { 93 | continue 94 | } // if 95 | mat[iA][iB] -= delCost(iA) + insCost(iB) 96 | if mat[iA][iB] > mx { 97 | mx = mat[iA][iB] 98 | } // if 99 | } // for c 100 | } // for r 101 | 102 | for { 103 | mn := mx + 1 104 | selA, selB := -1, -1 105 | for iA := range mat { 106 | if matA[iA] >= 0 { 107 | continue 108 | } // if 109 | for iB := 0; iB < lenB; iB++ { 110 | if matB[iB] >= 0 { 111 | continue 112 | } // if 113 | 114 | if mat[iA][iB] < mn { 115 | mn = mat[iA][iB] 116 | selA, selB = iA, iB 117 | } // if 118 | } // for iB 119 | } // for iA 120 | 121 | if selA < 0 || mn >= 0 { 122 | break 123 | } // if 124 | 125 | matA[selA] = selB 126 | matB[selB] = selA 127 | } // for 128 | 129 | for iA := range matA { 130 | if matA[iA] < 0 { 131 | cost += delCost(iA) 132 | } else { 133 | cost += diffMat[iA][matA[iA]] 134 | } // else 135 | } // for iA 136 | for iB := range matB { 137 | if matB[iB] < 0 { 138 | cost += insCost(iB) 139 | } // if 140 | } // for iB 141 | 142 | return diffMat, cost, matA, matB 143 | } 144 | 145 | const ( 146 | df_NONE = iota 147 | df_TYPE 148 | df_CONST 149 | df_VAR 150 | df_STRUCT 151 | df_INTERFACE 152 | df_FUNC 153 | df_STAR 154 | df_VAR_LINE 155 | df_PAIR 156 | df_NAMES 157 | df_VALUES 158 | df_BLOCK 159 | df_RESULTS 160 | ) 161 | 162 | var typeNames []string = []string{ 163 | "", 164 | "type", 165 | "const", 166 | "var", 167 | "struct", 168 | "interface", 169 | "func", 170 | "*", 171 | "", 172 | "", 173 | "", 174 | "", 175 | "", 176 | ""} 177 | 178 | type diffFragment interface { 179 | Type() int 180 | Weight() int 181 | 182 | // Max diff = this.Weight() + that.Weight() 183 | calcDiff(that diffFragment) int 184 | 185 | showDiff(that diffFragment) 186 | // indent is the leading chars from the second line 187 | sourceLines(indent string) []string 188 | oneLine() string 189 | } 190 | 191 | type fragment struct { 192 | tp int 193 | Parts []diffFragment 194 | } 195 | 196 | func (f *fragment) Type() int { 197 | return f.tp 198 | } 199 | 200 | func (f *fragment) Weight() (w int) { 201 | if f == nil { 202 | return 10 203 | } // if 204 | 205 | switch f.Type() { 206 | case df_FUNC: 207 | for i := 0; i < 4; i++ { 208 | w += f.Parts[i].Weight() 209 | } // for i 210 | w += int(math.Sqrt(float64(f.Parts[4].Weight())/100.) * 100) 211 | default: 212 | for _, p := range f.Parts { 213 | w += p.Weight() 214 | } // for p 215 | } 216 | 217 | switch f.Type() { 218 | case df_STAR: 219 | w += 50 220 | } 221 | return w 222 | } 223 | 224 | func catLines(a []string, sep string, b []string) []string { 225 | if len(a) > 0 && len(b) > 0 { 226 | b[0] = cat(a[len(a)-1], sep, b[0]) 227 | a = a[:len(a)-1] 228 | } 229 | 230 | return append(a, b...) 231 | } 232 | 233 | /* 234 | a[0] 235 | a[1] 236 | ... 237 | cat(a[end], sep, b[0]) 238 | b[1] 239 | ... 240 | b[end] 241 | */ 242 | func appendLines(a []string, sep string, b ...string) []string { 243 | if len(a) > 0 && len(b) > 0 { 244 | b[0] = cat(a[len(a)-1], sep, b[0]) 245 | a = a[:len(a)-1] 246 | } 247 | 248 | return append(a, b...) 249 | } 250 | 251 | func insertIndent(indent string, lines []string) []string { 252 | for i := range lines { 253 | lines[i] = indent + lines[i] 254 | } // for i 255 | 256 | return lines 257 | } 258 | 259 | func insertIndent2(indent string, lines []string) []string { 260 | for i := range lines { 261 | if i > 0 { 262 | lines[i] = indent + lines[i] 263 | } // if 264 | } // for i 265 | 266 | return lines 267 | } 268 | 269 | func (f *fragment) oneLine() string { 270 | if f == nil { 271 | return "" 272 | } 273 | switch f.tp { 274 | } 275 | lines := f.sourceLines("") 276 | if len(lines) == 0 { 277 | return "" 278 | } 279 | 280 | if len(lines) == 1 { 281 | return lines[0] 282 | } 283 | 284 | return lines[0] + " ... " + lines[len(lines)-1] + fmt.Sprintf(" (%d lines)", len(lines)) 285 | } 286 | 287 | func (f *fragment) sourceLines(indent string) (lines []string) { 288 | if f == nil { 289 | return nil 290 | } // if 291 | 292 | switch f.tp { 293 | case df_TYPE: 294 | lines = append(lines, typeNames[f.tp]) 295 | lines = catLines(lines, " ", f.Parts[0].sourceLines(indent)) 296 | lines = catLines(lines, " ", f.Parts[1].sourceLines(indent)) 297 | case df_CONST: 298 | if len(f.Parts) == 1 { 299 | lines = append(lines, typeNames[f.tp]) 300 | lines = catLines(lines, " ", f.Parts[0].sourceLines(indent)) 301 | } else { 302 | lines = append(lines, typeNames[f.tp]+"(") 303 | for _, p := range f.Parts { 304 | lines = append(lines, catLines([]string{indent + " "}, "", p.sourceLines(indent+" "))...) 305 | } // p 306 | lines = append(lines, indent+")") 307 | } // else 308 | case df_VAR: 309 | lines = append(lines, typeNames[f.tp]) 310 | lines = catLines(lines, " ", f.Parts[0].sourceLines(indent+" ")) 311 | case df_VAR_LINE: 312 | lines = f.Parts[0].sourceLines(indent) 313 | lines = catLines(lines, " ", f.Parts[1].sourceLines(indent)) 314 | lines = catLines(lines, " = ", f.Parts[2].sourceLines(indent)) 315 | case df_FUNC: 316 | lines = append(lines, typeNames[f.tp]) 317 | if f.Parts[0].(*fragment) != nil { 318 | lines = catLines(catLines(lines, " (", f.Parts[0].sourceLines(indent+" ")), "", []string{")"}) // recv 319 | } // if 320 | lines = catLines(lines, " ", f.Parts[1].sourceLines(indent+" ")) // name 321 | lines = catLines(catLines(catLines(lines, "", []string{"("}), "", 322 | f.Parts[2].sourceLines(indent+" ")), "", []string{")"}) // params 323 | lines = catLines(lines, " ", f.Parts[3].sourceLines(indent+" ")) // returns 324 | lines = catLines(lines, " ", f.Parts[4].sourceLines(indent)) // body 325 | case df_RESULTS: 326 | if len(f.Parts) > 0 { 327 | if len(f.Parts) > 1 || len(f.Parts[0].(*fragment).Parts[0].(*stringFrag).source) > 0 { 328 | lines = append(lines, "(") 329 | } // if 330 | for i, p := range f.Parts { 331 | if i > 0 { 332 | lines = catLines(lines, "", []string{", "}) 333 | } // if 334 | lines = catLines(lines, "", p.sourceLines(indent+" ")) 335 | } // for i, p 336 | if len(f.Parts) > 1 || len(f.Parts[0].(*fragment).Parts[0].(*stringFrag).source) > 0 { 337 | lines = catLines(lines, "", []string{")"}) 338 | } // if 339 | } // if 340 | case df_BLOCK: 341 | lines = append(lines, "{") 342 | for _, p := range f.Parts { 343 | lines = append(lines, catLines([]string{indent + " "}, "", p.sourceLines(indent+" "))...) 344 | } // for p 345 | lines = append(lines, indent+"}") 346 | case df_STRUCT, df_INTERFACE: 347 | if len(f.Parts) == 0 { 348 | lines = append(lines, typeNames[f.tp]+"{}") 349 | } else { 350 | lines = append(lines, typeNames[f.tp]+" {") 351 | for _, p := range f.Parts { 352 | lns := p.sourceLines(indent + " ") 353 | if len(lns) > 0 { 354 | lns[0] = indent + " " + lns[0] 355 | lines = append(lines, lns...) 356 | } // if 357 | } // for p 358 | lines = append(lines, indent+"}") 359 | } 360 | case df_STAR: 361 | lines = append(lines, typeNames[f.tp]) 362 | lines = catLines(lines, "", f.Parts[0].sourceLines(indent)) 363 | case df_PAIR: 364 | lines = catLines(f.Parts[0].sourceLines(indent), " ", f.Parts[1].sourceLines(indent)) 365 | case df_NAMES: 366 | s := "" 367 | for _, p := range f.Parts { 368 | s = cat(s, ", ", p.sourceLines(indent + " ")[0]) 369 | } // for p 370 | lines = append(lines, s) 371 | case df_VALUES: 372 | for _, p := range f.Parts { 373 | lines = catLines(lines, ", ", p.sourceLines(indent+" ")) 374 | } // for p 375 | case df_NONE: 376 | for _, p := range f.Parts { 377 | lines = append(lines, p.sourceLines(indent)...) 378 | } // for p 379 | default: 380 | lines = []string{"TYPE: " + typeNames[f.Type()]} 381 | for _, p := range f.Parts { 382 | lines = append(lines, p.sourceLines(indent+" ")...) 383 | } // for p 384 | } 385 | 386 | //f.lines = lines 387 | return lines 388 | } 389 | 390 | func (f *fragment) calcDiff(that diffFragment) int { 391 | switch g := that.(type) { 392 | case *fragment: 393 | if f == nil { 394 | if g == nil { 395 | return 0 396 | } else { 397 | return f.Weight() + g.Weight() 398 | } // else 399 | } // if 400 | if g == nil { 401 | return f.Weight() + g.Weight() 402 | } // if 403 | 404 | switch f.Type() { 405 | case df_STAR: 406 | if g.Type() == df_STAR { 407 | return f.Parts[0].calcDiff(g.Parts[0]) 408 | } // if 409 | 410 | return f.Parts[0].calcDiff(g) + 50 411 | } 412 | 413 | if g.Type() == df_STAR { 414 | return f.calcDiff(g.Parts[0]) + 50 415 | } // if 416 | 417 | if f.Type() != g.Type() { 418 | return f.Weight() + g.Weight() 419 | } // if 420 | 421 | switch f.Type() { 422 | case df_FUNC: 423 | res := int(0) 424 | for i := 0; i < 4; i++ { 425 | res += f.Parts[i].calcDiff(g.Parts[i]) 426 | } // for i 427 | 428 | res += int(math.Sqrt(float64(f.Parts[4].calcDiff(g.Parts[4]))/100.) * 100) 429 | 430 | return res 431 | } 432 | 433 | return ed.EditDistanceF(len(f.Parts), len(g.Parts), func(iA, iB int) int { 434 | return f.Parts[iA].calcDiff(g.Parts[iB]) * 3 / 2 435 | }, func(iA int) int { 436 | return f.Parts[iA].Weight() 437 | }, func(iB int) int { 438 | return g.Parts[iB].Weight() 439 | }) 440 | } 441 | return f.Weight() + that.Weight() 442 | } 443 | 444 | func (f *fragment) showDiff(that diffFragment) { 445 | diffLines(f.sourceLines(""), that.sourceLines(""), `%s`) 446 | } 447 | 448 | type stringFrag struct { 449 | weight int 450 | source string 451 | } 452 | 453 | func newStringFrag(source string, weight int) *stringFrag { 454 | return &stringFrag{weight: weight, source: source} 455 | } 456 | 457 | func (sf *stringFrag) Type() int { 458 | return df_NONE 459 | } 460 | 461 | func (sf *stringFrag) Weight() int { 462 | return sf.weight 463 | } 464 | 465 | func (sf *stringFrag) calcDiff(that diffFragment) int { 466 | switch g := that.(type) { 467 | case *stringFrag: 468 | s1, s2 := strings.TrimSpace(sf.source), strings.TrimSpace(g.source) 469 | if len(s1)+len(s2) == 0 { 470 | return 0 471 | } // if 472 | wt := sf.weight + g.weight 473 | return ed.String(s1, s2) * wt / mathp.MaxI(len(s1), len(s2)) 474 | } // switch 475 | 476 | return sf.Weight() + that.Weight() 477 | } 478 | 479 | func (sf *stringFrag) showDiff(that diffFragment) { 480 | diffLines(sf.sourceLines(" "), that.sourceLines(" "), `%s`) 481 | } 482 | 483 | func (sf *stringFrag) oneLine() string { 484 | if sf == nil { 485 | return "" 486 | } // if 487 | 488 | return sf.source 489 | } 490 | 491 | func (sf *stringFrag) sourceLines(indent string) []string { 492 | lines := strings.Split(sf.source, "\n") 493 | for i := range lines { 494 | if i > 0 { 495 | lines[i] = indent + lines[i] 496 | } // if 497 | } // for i 498 | 499 | return lines 500 | } 501 | 502 | const ( 503 | td_STRUCT = iota 504 | td_INTERFACE 505 | td_POINTER 506 | td_ONELINE 507 | ) 508 | 509 | func newNameTypes(fs *token.FileSet, fl *ast.FieldList) (dfs []diffFragment) { 510 | for _, f := range fl.List { 511 | if len(f.Names) > 0 { 512 | for _, name := range f.Names { 513 | dfs = append(dfs, &fragment{tp: df_PAIR, 514 | Parts: []diffFragment{newStringFrag(name.String(), 100), 515 | newTypeDef(fs, f.Type)}}) 516 | } // for name 517 | } else { 518 | // embedding 519 | dfs = append(dfs, &fragment{tp: df_PAIR, 520 | Parts: []diffFragment{newStringFrag("", 50), 521 | newTypeDef(fs, f.Type)}}) 522 | } // else 523 | } // for f 524 | 525 | return dfs 526 | } 527 | 528 | func newTypeDef(fs *token.FileSet, def ast.Expr) diffFragment { 529 | switch d := def.(type) { 530 | case *ast.StructType: 531 | return &fragment{tp: df_STRUCT, Parts: newNameTypes(fs, d.Fields)} 532 | 533 | case *ast.InterfaceType: 534 | return &fragment{tp: df_INTERFACE, Parts: newNameTypes(fs, d.Methods)} 535 | 536 | case *ast.StarExpr: 537 | return &fragment{tp: df_STAR, Parts: []diffFragment{newTypeDef(fs, d.X)}} 538 | } // switch 539 | 540 | var src bytes.Buffer 541 | (&printer.Config{Mode: printer.UseSpaces, Tabwidth: 4}).Fprint(&src, fs, def) 542 | return &stringFrag{weight: 50, source: src.String()} 543 | } 544 | 545 | func newTypeStmtInfo(fs *token.FileSet, name string, def ast.Expr) *fragment { 546 | var f fragment 547 | 548 | f.tp = df_TYPE 549 | f.Parts = []diffFragment{ 550 | newStringFrag(name, 100), 551 | newTypeDef(fs, def)} 552 | 553 | return &f 554 | } 555 | 556 | func newExpDef(fs *token.FileSet, def ast.Expr) diffFragment { 557 | //ast.Print(fs, def) 558 | var src bytes.Buffer 559 | (&printer.Config{Mode: printer.UseSpaces, Tabwidth: 4}).Fprint(&src, fs, def) 560 | return &stringFrag{weight: 100, source: src.String()} 561 | } 562 | 563 | func newVarSpecs(fs *token.FileSet, specs []ast.Spec) (dfs []diffFragment) { 564 | for _, spec := range specs { 565 | f := &fragment{tp: df_VAR_LINE} 566 | 567 | names := &fragment{tp: df_NAMES} 568 | sp := spec.(*ast.ValueSpec) 569 | for _, name := range sp.Names { 570 | names.Parts = append(names.Parts, &stringFrag{weight: 100, 571 | source: fmt.Sprint(name)}) 572 | } 573 | f.Parts = append(f.Parts, names) 574 | 575 | if sp.Type != nil { 576 | f.Parts = append(f.Parts, newTypeDef(fs, sp.Type)) 577 | } else { 578 | f.Parts = append(f.Parts, (*fragment)(nil)) 579 | } // else 580 | 581 | values := &fragment{tp: df_VALUES} 582 | for _, v := range sp.Values { 583 | values.Parts = append(values.Parts, newExpDef(fs, v)) 584 | } // for v 585 | f.Parts = append(f.Parts, values) 586 | 587 | dfs = append(dfs, f) 588 | } 589 | 590 | return dfs 591 | } 592 | 593 | func printToLines(fs *token.FileSet, node interface{}) []string { 594 | var src bytes.Buffer 595 | (&printer.Config{Mode: printer.UseSpaces, Tabwidth: 4}).Fprint(&src, fs, node) 596 | return strings.Split(src.String(), "\n") 597 | } 598 | 599 | func nodeToLines(fs *token.FileSet, node interface{}) (lines []string) { 600 | switch nd := node.(type) { 601 | case *ast.IfStmt: 602 | lines = append(lines, "if") 603 | if nd.Init != nil { 604 | lines = appendLines(lines, " ", nodeToLines(fs, nd.Init)...) 605 | lines = appendLines(lines, "", ";") 606 | } // if 607 | 608 | lines = catLines(lines, " ", nodeToLines(fs, nd.Cond)) 609 | lines = catLines(lines, " ", []string{"{"}) 610 | lines = append(lines, insertIndent(" ", blockToLines(fs, nd.Body))...) 611 | lines = append(lines, "}") 612 | if nd.Else != nil { 613 | //ast.Print(fs, st.Else) 614 | lines = catLines(lines, "", []string{" else "}) 615 | lines = catLines(lines, "", nodeToLines(fs, nd.Else)) 616 | } // if 617 | case *ast.AssignStmt: 618 | for _, exp := range nd.Lhs { 619 | lines = catLines(lines, ", ", nodeToLines(fs, exp)) 620 | } // for i 621 | 622 | lines = catLines(lines, "", []string{" " + nd.Tok.String() + " "}) 623 | 624 | for i, exp := range nd.Rhs { 625 | if i > 0 { 626 | lines = catLines(lines, "", []string{", "}) 627 | } // if 628 | lines = catLines(lines, "", nodeToLines(fs, exp)) 629 | } // for i 630 | 631 | case *ast.ForStmt: 632 | lines = append(lines, "for") 633 | if nd.Cond != nil { 634 | lns := []string{} 635 | if nd.Init != nil { 636 | lns = catLines(lns, "; ", nodeToLines(fs, nd.Init)) 637 | } // if 638 | lns = catLines(lns, "; ", nodeToLines(fs, nd.Cond)) 639 | if nd.Post != nil { 640 | lns = catLines(lns, "; ", nodeToLines(fs, nd.Post)) 641 | } // if 642 | 643 | lines = catLines(lines, " ", lns) 644 | } // if 645 | lines = catLines(lines, "", []string{" {"}) 646 | lines = append(lines, insertIndent(" ", blockToLines(fs, nd.Body))...) 647 | lines = append(lines, "}") 648 | case *ast.RangeStmt: 649 | lines = append(lines, "for") 650 | lines = catLines(lines, " ", nodeToLines(fs, nd.Key)) 651 | if nd.Value != nil { 652 | lines = catLines(lines, ", ", nodeToLines(fs, nd.Value)) 653 | } // if 654 | lines = catLines(lines, "", []string{" " + nd.Tok.String() + " "}) 655 | lines = catLines(lines, "", []string{" range"}) 656 | lines = catLines(lines, " ", nodeToLines(fs, nd.X)) 657 | lines = catLines(lines, "", []string{" {"}) 658 | lines = append(lines, insertIndent(" ", blockToLines(fs, nd.Body))...) 659 | lines = append(lines, "}") 660 | 661 | case *ast.BlockStmt: 662 | lines = append(lines, "{") 663 | lines = append(lines, insertIndent(" ", blockToLines(fs, nd))...) 664 | lines = append(lines, "}") 665 | 666 | case *ast.ReturnStmt: 667 | lines = append(lines, "return") 668 | if nd.Results != nil { 669 | for i, e := range nd.Results { 670 | if i == 0 { 671 | lines = appendLines(lines, " ", nodeToLines(fs, e)...) 672 | } else { 673 | lines = appendLines(lines, ", ", nodeToLines(fs, e)...) 674 | } // else 675 | } // for i, e 676 | } // if 677 | 678 | case *ast.DeferStmt: 679 | lines = append(lines, "defer") 680 | lines = appendLines(lines, " ", nodeToLines(fs, nd.Call)...) 681 | 682 | case *ast.GoStmt: 683 | lines = append(lines, "go") 684 | lines = appendLines(lines, " ", nodeToLines(fs, nd.Call)...) 685 | 686 | case *ast.SendStmt: 687 | lines = append(lines, nodeToLines(fs, nd.Chan)...) 688 | lines = appendLines(lines, " ", "<-") 689 | lines = appendLines(lines, " ", nodeToLines(fs, nd.Value)...) 690 | 691 | case *ast.ExprStmt: 692 | return nodeToLines(fs, nd.X) 693 | 694 | case *ast.EmptyStmt: 695 | // Do nothing 696 | 697 | case *ast.SwitchStmt: 698 | lines = append(lines, "switch") 699 | if nd.Init != nil { 700 | lines = appendLines(lines, " ", nodeToLines(fs, nd.Init)...) 701 | lines = appendLines(lines, "", ";") 702 | } // if 703 | if nd.Tag != nil { 704 | lines = appendLines(lines, " ", nodeToLines(fs, nd.Tag)...) 705 | } // if 706 | lines = appendLines(lines, " ", nodeToLines(fs, nd.Body)...) 707 | 708 | case *ast.TypeSwitchStmt: 709 | lines = append(lines, "switch") 710 | if nd.Init != nil { 711 | lines = appendLines(lines, " ", nodeToLines(fs, nd.Init)...) 712 | lines = appendLines(lines, "", ";") 713 | } // if 714 | lines = appendLines(lines, " ", nodeToLines(fs, nd.Assign)...) 715 | lines = appendLines(lines, " ", nodeToLines(fs, nd.Body)...) 716 | 717 | case *ast.CompositeLit: 718 | if nd.Type != nil { 719 | lines = append(lines, printToLines(fs, nd.Type)...) 720 | } 721 | if len(nd.Elts) == 0 { 722 | // short form 723 | lines = appendLines(lines, "", "{}") 724 | } else { 725 | lines = appendLines(lines, "", "{") 726 | 727 | for _, el := range nd.Elts { 728 | lines = append(lines, insertIndent(" ", nodeToLines(fs, el))...) 729 | lines = appendLines(lines, "", ",") 730 | } 731 | // put } in a new line 732 | lines = append(lines, "") 733 | lines = appendLines(lines, "", "}") 734 | } 735 | 736 | case *ast.UnaryExpr: 737 | lines = append(lines, nd.Op.String()) 738 | lines = appendLines(lines, "", nodeToLines(fs, nd.X)...) 739 | 740 | case *ast.BinaryExpr: 741 | lines = appendLines(lines, "", nodeToLines(fs, nd.X)...) 742 | lines = appendLines(lines, " ", nd.Op.String()) 743 | lines = appendLines(lines, " ", nodeToLines(fs, nd.Y)...) 744 | 745 | case *ast.ParenExpr: 746 | lines = append(lines, "(") 747 | lines = appendLines(lines, "", nodeToLines(fs, nd.X)...) 748 | lines = appendLines(lines, "", ")") 749 | 750 | case *ast.CallExpr: 751 | lines = append(lines, nodeToLines(fs, nd.Fun)...) 752 | lines = appendLines(lines, "", "(") 753 | for i, a := range nd.Args { 754 | if i > 0 { 755 | lines = appendLines(lines, "", ", ") 756 | } // if 757 | lines = appendLines(lines, "", nodeToLines(fs, a)...) 758 | } // for i, el 759 | if nd.Ellipsis > 0 { 760 | lines = appendLines(lines, "", "...") 761 | } // if 762 | lines = appendLines(lines, "", ")") 763 | case *ast.KeyValueExpr: 764 | lines = append(lines, nodeToLines(fs, nd.Key)...) 765 | lines = appendLines(lines, ": ", nodeToLines(fs, nd.Value)...) 766 | case *ast.FuncLit: 767 | lines = nodeToLines(fs, nd.Type) 768 | lines = appendLines(lines, " ", nodeToLines(fs, nd.Body)...) 769 | 770 | case *ast.CaseClause: 771 | if nd.List == nil { 772 | lines = append(lines, "default:") 773 | } else { 774 | lines = append(lines, "case ") 775 | for i, e := range nd.List { 776 | if i > 0 { 777 | lines = appendLines(lines, "", ", ") 778 | } // if 779 | lines = appendLines(lines, "", nodeToLines(fs, e)...) 780 | } // for i 781 | lines = appendLines(lines, "", ":") 782 | } // else 783 | 784 | for _, st := range nd.Body { 785 | lines = append(lines, insertIndent(" ", nodeToLines(fs, st))...) 786 | } // for 787 | 788 | case *ast.SelectorExpr: 789 | lines = append(lines, nodeToLines(fs, nd.X)...) 790 | lines = appendLines(lines, "", ".") 791 | lines = appendLines(lines, "", nodeToLines(fs, nd.Sel)...) 792 | case *ast.LabeledStmt: 793 | lines = append(lines, nodeToLines(fs, nd.Label)...) 794 | lines = appendLines(lines, "", ":") 795 | lines = appendLines(lines, "", nodeToLines(fs, nd.Stmt)...) 796 | case *ast.Ident, *ast.BasicLit, *ast.DeclStmt, *ast.BranchStmt, *ast.IndexExpr, *ast.FuncType, *ast.SliceExpr, *ast.StarExpr, *ast.ArrayType, *ast.TypeAssertExpr: 797 | return printToLines(fs, nd) 798 | default: 799 | //ast.Print(fs, nd) 800 | //ast.Print(fs, printToLines(fs, nd)) 801 | 802 | return printToLines(fs, nd) 803 | } 804 | 805 | return lines 806 | } 807 | 808 | func blockToLines(fs *token.FileSet, blk *ast.BlockStmt) (lines []string) { 809 | for _, s := range blk.List { 810 | lines = append(lines, nodeToLines(fs, s)...) 811 | } // for s 812 | 813 | return lines 814 | } 815 | 816 | func newBlockDecl(fs *token.FileSet, blk *ast.BlockStmt) (f *fragment) { 817 | f = &fragment{tp: df_BLOCK} 818 | lines := blockToLines(fs, blk) 819 | for _, line := range lines { 820 | f.Parts = append(f.Parts, &stringFrag{weight: 100, source: line}) 821 | } // for line 822 | 823 | return f 824 | } 825 | 826 | func newFuncDecl(fs *token.FileSet, d *ast.FuncDecl) (f *fragment) { 827 | f = &fragment{tp: df_FUNC} 828 | 829 | // recv 830 | if d.Recv != nil { 831 | f.Parts = append(f.Parts, newNameTypes(fs, d.Recv)...) 832 | } else { 833 | f.Parts = append(f.Parts, (*fragment)(nil)) 834 | } // else 835 | 836 | // name 837 | f.Parts = append(f.Parts, &stringFrag{weight: 200, source: fmt.Sprint(d.Name)}) 838 | 839 | // params 840 | if d.Type.Params != nil { 841 | f.Parts = append(f.Parts, &fragment{tp: df_VALUES, 842 | Parts: newNameTypes(fs, d.Type.Params)}) 843 | } else { 844 | f.Parts = append(f.Parts, (*fragment)(nil)) 845 | } // else 846 | 847 | // Results 848 | if d.Type.Results != nil { 849 | f.Parts = append(f.Parts, &fragment{tp: df_RESULTS, Parts: newNameTypes(fs, d.Type.Results)}) 850 | } else { 851 | f.Parts = append(f.Parts, (*fragment)(nil)) 852 | } // else 853 | 854 | // body 855 | if d.Body != nil { 856 | f.Parts = append(f.Parts, newBlockDecl(fs, d.Body)) 857 | } else { 858 | f.Parts = append(f.Parts, (*fragment)(nil)) 859 | } // else 860 | return f 861 | } 862 | 863 | type fileInfo struct { 864 | f *ast.File 865 | fs *token.FileSet 866 | types *fragment 867 | vars *fragment 868 | funcs *fragment 869 | } 870 | 871 | func (info *fileInfo) collect() { 872 | info.types = &fragment{} 873 | info.vars = &fragment{} 874 | info.funcs = &fragment{} 875 | 876 | for _, decl := range info.f.Decls { 877 | switch d := decl.(type) { 878 | case *ast.GenDecl: 879 | switch d.Tok { 880 | case token.TYPE: 881 | for i := range d.Specs { 882 | spec := d.Specs[i].(*ast.TypeSpec) 883 | //ast.Print(info.fs, spec) 884 | ti := newTypeStmtInfo(info.fs, spec.Name.String(), spec.Type) 885 | info.types.Parts = append(info.types.Parts, ti) 886 | } // for i 887 | case token.CONST: 888 | // fmt.Println(d) 889 | //ast.Print(info.fs, d) 890 | v := &fragment{tp: df_CONST, Parts: newVarSpecs(info.fs, d.Specs)} 891 | info.vars.Parts = append(info.vars.Parts, v) 892 | case token.VAR: 893 | //ast.Print(info.fs, d) 894 | vss := newVarSpecs(info.fs, d.Specs) 895 | for _, vs := range vss { 896 | info.vars.Parts = append(info.vars.Parts, &fragment{tp: df_VAR, Parts: []diffFragment{vs}}) 897 | } 898 | case token.IMPORT: 899 | // ignore 900 | default: 901 | // Unknow 902 | fmt.Fprintln(gOut, d) 903 | } // switch d.tok 904 | case *ast.FuncDecl: 905 | fd := newFuncDecl(info.fs, d) 906 | info.funcs.Parts = append(info.funcs.Parts, fd) 907 | //ast.Print(info.fs, d) 908 | default: 909 | // fmt.Println(d) 910 | } // switch decl.(type) 911 | } // for decl 912 | } 913 | 914 | func parse(fn string, src interface{}) (*fileInfo, error) { 915 | if fn == "/dev/null" { 916 | return &fileInfo{ 917 | f: &ast.File{}, 918 | types: &fragment{}, 919 | vars: &fragment{}, 920 | funcs: &fragment{}, 921 | }, nil 922 | } 923 | 924 | fset := token.NewFileSet() 925 | f, err := parser.ParseFile(fset, fn, src, 0) 926 | if err != nil { 927 | return nil, err 928 | } // if 929 | 930 | info := &fileInfo{f: f, fs: fset} 931 | info.collect() 932 | 933 | return info, nil 934 | } 935 | 936 | const ( 937 | // delete color 938 | del_COLOR = ct.Red 939 | // insert color 940 | ins_COLOR = ct.Green 941 | // matched color 942 | mat_COLOR = ct.White 943 | // folded color 944 | fld_COLOR = ct.Yellow 945 | ) 946 | 947 | func showDelWholeLine(line string) { 948 | changeColor(del_COLOR, false, ct.None, false) 949 | fmt.Fprintln(gOut, "===", line) 950 | resetColor() 951 | } 952 | func showDelLine(line string) { 953 | changeColor(del_COLOR, false, ct.None, false) 954 | fmt.Fprintln(gOut, "---", line) 955 | resetColor() 956 | } 957 | func showColorDelLine(line, lcs string) { 958 | changeColor(del_COLOR, false, ct.None, false) 959 | fmt.Fprint(gOut, "--- ") 960 | lcsr := []rune(lcs) 961 | for _, c := range line { 962 | if len(lcsr) > 0 && lcsr[0] == c { 963 | resetColor() 964 | lcsr = lcsr[1:] 965 | } else { 966 | changeColor(del_COLOR, false, ct.None, false) 967 | } // else 968 | fmt.Fprintf(gOut, "%c", c) 969 | } // for c 970 | 971 | resetColor() 972 | fmt.Fprintln(gOut) 973 | } 974 | 975 | func showDelLines(lines []string, gapLines int) { 976 | if len(lines) <= gapLines*2+1 { 977 | for _, line := range lines { 978 | showDelLine(line) 979 | } // for line 980 | return 981 | } // if 982 | 983 | for i, line := range lines { 984 | if i < gapLines || i >= len(lines)-gapLines { 985 | showDelLine(line) 986 | } // if 987 | if i == gapLines { 988 | showDelWholeLine(fmt.Sprintf(" ... (%d lines)", len(lines)-gapLines*2)) 989 | } // if 990 | } // for i 991 | } 992 | 993 | func showInsLine(line string) { 994 | changeColor(ins_COLOR, false, ct.None, false) 995 | fmt.Fprintln(gOut, "+++", line) 996 | resetColor() 997 | } 998 | func showColorInsLine(line, lcs string) { 999 | changeColor(ins_COLOR, false, ct.None, false) 1000 | fmt.Fprint(gOut, "+++ ") 1001 | lcsr := []rune(lcs) 1002 | for _, c := range line { 1003 | if len(lcsr) > 0 && lcsr[0] == c { 1004 | resetColor() 1005 | lcsr = lcsr[1:] 1006 | } else { 1007 | changeColor(ins_COLOR, false, ct.None, false) 1008 | } // else 1009 | fmt.Fprintf(gOut, "%c", c) 1010 | } // for c 1011 | 1012 | resetColor() 1013 | fmt.Fprintln(gOut) 1014 | } 1015 | 1016 | func showInsWholeLine(line string) { 1017 | changeColor(ins_COLOR, false, ct.None, false) 1018 | fmt.Fprintln(gOut, "###", line) 1019 | resetColor() 1020 | } 1021 | 1022 | func showInsLines(lines []string, gapLines int) { 1023 | if len(lines) <= gapLines*2+1 { 1024 | for _, line := range lines { 1025 | showInsLine(line) 1026 | } // for line 1027 | return 1028 | } // if 1029 | 1030 | for i, line := range lines { 1031 | if i < gapLines || i >= len(lines)-gapLines { 1032 | showInsLine(line) 1033 | } // if 1034 | if i == gapLines { 1035 | showInsWholeLine(fmt.Sprintf(" ... (%d lines)", len(lines)-gapLines*2)) 1036 | } // if 1037 | } // for i 1038 | } 1039 | 1040 | func showDelTokens(del []string, mat []int, ins []string) { 1041 | changeColor(del_COLOR, false, ct.None, false) 1042 | fmt.Fprint(gOut, "--- ") 1043 | 1044 | for i, tk := range del { 1045 | if mat[i] < 0 || tk != ins[mat[i]] { 1046 | changeColor(del_COLOR, false, ct.None, false) 1047 | } else { 1048 | changeColor(mat_COLOR, false, ct.None, false) 1049 | } 1050 | 1051 | fmt.Fprint(gOut, tk) 1052 | } // for i 1053 | 1054 | resetColor() 1055 | fmt.Fprintln(gOut) 1056 | } 1057 | 1058 | func showInsTokens(ins []string, mat []int, del []string) { 1059 | changeColor(ins_COLOR, false, ct.None, false) 1060 | fmt.Fprint(gOut, "+++ ") 1061 | 1062 | for i, tk := range ins { 1063 | if mat[i] < 0 || tk != del[mat[i]] { 1064 | changeColor(ins_COLOR, false, ct.None, false) 1065 | } else { 1066 | resetColor() 1067 | } // else 1068 | 1069 | fmt.Fprint(gOut, tk) 1070 | } // for i 1071 | 1072 | resetColor() 1073 | fmt.Fprintln(gOut) 1074 | } 1075 | 1076 | func showDiffLine(del, ins string) { 1077 | delT, insT := tm.LineToTokens(del), tm.LineToTokens(ins) 1078 | matA, matB := tm.MatchTokens(delT, insT) 1079 | 1080 | showDelTokens(delT, matA, insT) 1081 | showInsTokens(insT, matB, delT) 1082 | } 1083 | 1084 | func diffLineSet(orgLines, newLines []string, format string) { 1085 | sort.Strings(orgLines) 1086 | sort.Strings(newLines) 1087 | 1088 | _, matA, matB := ed.EditDistanceFFull(len(orgLines), len(newLines), func(iA, iB int) int { 1089 | return tm.DiffOfStrings(orgLines[iA], newLines[iB], 4000) 1090 | }, ed.ConstCost(1000), ed.ConstCost(1000)) 1091 | 1092 | for i, j := 0, 0; i < len(orgLines) || j < len(newLines); { 1093 | switch { 1094 | case j >= len(newLines) || i < len(orgLines) && matA[i] < 0: 1095 | showDelLine(fmt.Sprintf(format, orgLines[i])) 1096 | i++ 1097 | case i >= len(orgLines) || j < len(newLines) && matB[j] < 0: 1098 | showInsLine(fmt.Sprintf(format, newLines[j])) 1099 | j++ 1100 | default: 1101 | if strings.TrimSpace(orgLines[i]) != strings.TrimSpace(newLines[j]) { 1102 | showDiffLine(fmt.Sprintf(format, orgLines[i]), fmt.Sprintf(format, newLines[j])) 1103 | } // if 1104 | i++ 1105 | j++ 1106 | } 1107 | } // for i, j 1108 | } 1109 | 1110 | type lineOutputer interface { 1111 | outputIns(line string) 1112 | outputDel(line string) 1113 | outputSame(line string) 1114 | outputChange(del, ins string) 1115 | end() 1116 | } 1117 | 1118 | type lineOutput struct { 1119 | sameLines []string 1120 | } 1121 | 1122 | func (lo *lineOutput) outputIns(line string) { 1123 | lo.end() 1124 | showInsLine(line) 1125 | } 1126 | 1127 | func (lo *lineOutput) outputDel(line string) { 1128 | lo.end() 1129 | showDelLine(line) 1130 | } 1131 | 1132 | func (lo *lineOutput) outputChange(del, ins string) { 1133 | lo.end() 1134 | showDiffLine(del, ins) 1135 | } 1136 | 1137 | func (lo *lineOutput) outputSame(line string) { 1138 | lo.sameLines = append(lo.sameLines, line) 1139 | } 1140 | 1141 | func (lo *lineOutput) end() { 1142 | if len(lo.sameLines) > 0 { 1143 | fmt.Fprintln(gOut, " ", lo.sameLines[0]) 1144 | if len(lo.sameLines) == 3 { 1145 | fmt.Fprintln(gOut, " ", lo.sameLines[1]) 1146 | } // if 1147 | if len(lo.sameLines) > 3 { 1148 | changeColor(fld_COLOR, false, ct.None, false) 1149 | fmtp.Fprintfln(gOut, " ... (%d lines)", len(lo.sameLines)-2) 1150 | resetColor() 1151 | } // if 1152 | if len(lo.sameLines) > 1 { 1153 | fmt.Fprintln(gOut, " ", lo.sameLines[len(lo.sameLines)-1]) 1154 | } // if 1155 | } // if 1156 | 1157 | lo.sameLines = nil 1158 | } 1159 | 1160 | func offsetHeadTails(orgLines, newLines []string) (start, orgEnd, newEnd int) { 1161 | start = 0 1162 | for start < len(orgLines) && start < len(newLines) && orgLines[start] == newLines[start] { 1163 | start++ 1164 | } 1165 | 1166 | orgEnd, newEnd = len(orgLines), len(newLines) 1167 | for orgEnd > start && newEnd > start && orgLines[orgEnd-1] == newLines[newEnd-1] { 1168 | orgEnd-- 1169 | newEnd-- 1170 | } 1171 | return 1172 | } 1173 | 1174 | func isBlockStart(s string) bool { 1175 | return strings.HasPrefix(s, "{") || strings.HasPrefix(s, "/*") 1176 | } 1177 | 1178 | func diffLinesTo(orgLines, newLines []string, format string, lo lineOutputer) int { 1179 | if len(orgLines)+len(newLines) == 0 { 1180 | return 0 1181 | } 1182 | 1183 | start, orgEnd, newEnd := 0, len(orgLines), len(newLines) 1184 | 1185 | if len(orgLines)*len(newLines) > 1024*1024 { 1186 | // Use trivial comparison to offset same head and tail lines. 1187 | start, orgEnd, newEnd = offsetHeadTails(orgLines, newLines) 1188 | } 1189 | 1190 | fastMode := false 1191 | if len(orgLines)*len(newLines) > 1024*1024 { 1192 | fastMode = true 1193 | } 1194 | 1195 | _, matA, matB := ed.EditDistanceFFull(orgEnd-start, newEnd-start, func(iA, iB int) int { 1196 | sa, sb := orgLines[iA+start], newLines[iB+start] 1197 | rawEqual := sa == sb 1198 | sa, sb = strings.TrimSpace(sa), strings.TrimSpace(sb) 1199 | 1200 | posCost := 0 1201 | if isBlockStart(sa) { 1202 | posCost += (newEnd - start - 1 - iB) * 10 / (newEnd - start) 1203 | } 1204 | 1205 | if rawEqual { 1206 | return posCost 1207 | } 1208 | if sa == sb { 1209 | return posCost + 1 1210 | } 1211 | 1212 | mx := (len(sa) + len(sb)) * 150 1213 | 1214 | var dist int 1215 | 1216 | if fastMode && len(sa) > 10*len(sb) { 1217 | dist = 100 * (len(sa) - len(sb)) 1218 | } else if fastMode && len(sb) > 10*len(sa) { 1219 | dist = 100 * (len(sb) - len(sa)) 1220 | } else { 1221 | // When sa and sb has 1/3 in common, convertion const is equal to del+ins const 1222 | dist = tm.CalcDiffOfSourceLine(sa, sb, mx) 1223 | } 1224 | // Even a small change, both lines will be shown, so add a 20% penalty on that. 1225 | return (dist*4+mx)/5 + 1 + posCost 1226 | }, func(iA int) int { 1227 | return mathp.MaxI(1, len(strings.TrimSpace(orgLines[iA+start]))*100) 1228 | }, func(iB int) int { 1229 | return mathp.MaxI(1, len(strings.TrimSpace(newLines[iB+start]))*100) 1230 | }) 1231 | 1232 | cnt := 0 1233 | 1234 | for i, j := 0, 0; i < len(orgLines) || j < len(newLines); { 1235 | switch { 1236 | case i < start || i >= orgEnd && j >= newEnd: 1237 | // cut by offsetHeadTails 1238 | lo.outputSame(fmt.Sprintf(format, newLines[j])) 1239 | i++ 1240 | j++ 1241 | case j >= newEnd || i < orgEnd && matA[i-start] < 0: 1242 | lo.outputDel(fmt.Sprintf(format, orgLines[i])) 1243 | cnt++ 1244 | i++ 1245 | case i >= orgEnd || j < newEnd && matB[j-start] < 0: 1246 | lo.outputIns(fmt.Sprintf(format, newLines[j])) 1247 | cnt++ 1248 | j++ 1249 | default: 1250 | if strings.TrimSpace(orgLines[i]) != strings.TrimSpace(newLines[j]) { 1251 | lo.outputChange(fmt.Sprintf(format, orgLines[i]), fmt.Sprintf(format, newLines[j])) 1252 | cnt += 2 1253 | } else { 1254 | lo.outputSame(fmt.Sprintf(format, newLines[j])) 1255 | } // else 1256 | i++ 1257 | j++ 1258 | } 1259 | } 1260 | lo.end() 1261 | return cnt 1262 | } 1263 | 1264 | /* 1265 | Returns the number of operation lines. 1266 | */ 1267 | func diffLines(orgLines, newLines []string, format string) int { 1268 | return diffLinesTo(orgLines, newLines, format, &lineOutput{}) 1269 | } 1270 | 1271 | /* 1272 | Diff Package 1273 | */ 1274 | func diffPackage(orgInfo, newInfo *fileInfo) { 1275 | orgName := orgInfo.f.Name.String() 1276 | newName := newInfo.f.Name.String() 1277 | if orgName != newName { 1278 | showDiffLine("package "+orgName, "package "+newName) 1279 | } // if 1280 | } 1281 | 1282 | /* 1283 | Diff Imports 1284 | */ 1285 | func extractImports(info *fileInfo) []string { 1286 | imports := make([]string, 0, len(info.f.Imports)) 1287 | for _, imp := range info.f.Imports { 1288 | imports = append(imports, imp.Path.Value) 1289 | } // for imp 1290 | 1291 | return imports 1292 | } 1293 | 1294 | func diffImports(orgInfo, newInfo *fileInfo) { 1295 | orgImports := extractImports(orgInfo) 1296 | newImports := extractImports(newInfo) 1297 | 1298 | diffLineSet(orgImports, newImports, `import %s`) 1299 | } 1300 | 1301 | /* 1302 | Diff Types 1303 | */ 1304 | func diffTypes(orgInfo, newInfo *fileInfo) { 1305 | mat, _, matA, matB := greedyMatch(len(orgInfo.types.Parts), len(newInfo.types.Parts), func(iA, iB int) int { 1306 | return orgInfo.types.Parts[iA].calcDiff(newInfo.types.Parts[iB]) * 3 / 2 1307 | }, func(iA int) int { 1308 | return orgInfo.types.Parts[iA].Weight() 1309 | }, func(iB int) int { 1310 | return newInfo.types.Parts[iB].Weight() 1311 | }) 1312 | 1313 | j0 := 0 1314 | for i := range matA { 1315 | j := matA[i] 1316 | if j < 0 { 1317 | showDelWholeLine(orgInfo.types.Parts[i].oneLine()) 1318 | } else { 1319 | for ; j0 < j; j0++ { 1320 | if matB[j0] < 0 { 1321 | showInsWholeLine(newInfo.types.Parts[j0].oneLine()) 1322 | } 1323 | } 1324 | 1325 | if mat[i][j] > 0 { 1326 | orgInfo.types.Parts[i].showDiff(newInfo.types.Parts[j]) 1327 | } // if 1328 | } // else 1329 | } // for i 1330 | 1331 | for ; j0 < len(matB); j0++ { 1332 | if matB[j0] < 0 { 1333 | showInsWholeLine(newInfo.types.Parts[j0].oneLine()) 1334 | } 1335 | } 1336 | } 1337 | 1338 | func diffVars(orgInfo, newInfo *fileInfo) { 1339 | mat, _, matA, matB := greedyMatch(len(orgInfo.vars.Parts), len(newInfo.vars.Parts), func(iA, iB int) int { 1340 | return orgInfo.vars.Parts[iA].calcDiff(newInfo.vars.Parts[iB]) * 3 / 2 1341 | }, func(iA int) int { 1342 | return orgInfo.vars.Parts[iA].Weight() 1343 | }, func(iB int) int { 1344 | return newInfo.vars.Parts[iB].Weight() 1345 | }) 1346 | 1347 | j0 := 0 1348 | for i := range matA { 1349 | j := matA[i] 1350 | if j < 0 { 1351 | showDelLines(orgInfo.vars.Parts[i].sourceLines(""), 2) 1352 | // fmt.Println() 1353 | } else { 1354 | for ; j0 < j; j0++ { 1355 | if matB[j0] < 0 { 1356 | showInsLines(newInfo.vars.Parts[j0].sourceLines(""), 2) 1357 | } // if 1358 | } 1359 | 1360 | if mat[i][j] > 0 { 1361 | orgInfo.vars.Parts[i].showDiff(newInfo.vars.Parts[j]) 1362 | fmt.Fprintln(gOut) 1363 | } // if 1364 | } // else 1365 | } // for i 1366 | 1367 | for ; j0 < len(matB); j0++ { 1368 | if matB[j0] < 0 { 1369 | showInsLines(newInfo.vars.Parts[j0].sourceLines(""), 2) 1370 | } // if 1371 | } 1372 | } 1373 | 1374 | func diffFuncs(orgInfo, newInfo *fileInfo) { 1375 | mat, _, matA, matB := greedyMatch(len(orgInfo.funcs.Parts), len(newInfo.funcs.Parts), func(iA, iB int) int { 1376 | return orgInfo.funcs.Parts[iA].calcDiff(newInfo.funcs.Parts[iB]) * 3 / 2 1377 | }, func(iA int) int { 1378 | return orgInfo.funcs.Parts[iA].Weight() 1379 | }, func(iB int) int { 1380 | return newInfo.funcs.Parts[iB].Weight() 1381 | }) 1382 | 1383 | j0 := 0 1384 | for i := range matA { 1385 | j := matA[i] 1386 | if j < 0 { 1387 | showDelWholeLine(orgInfo.funcs.Parts[i].oneLine()) 1388 | } else { 1389 | for ; j0 < j; j0++ { 1390 | if matB[j0] < 0 { 1391 | showInsWholeLine(newInfo.funcs.Parts[j0].oneLine()) 1392 | } 1393 | } 1394 | if mat[i][j] > 0 { 1395 | orgInfo.funcs.Parts[i].showDiff(newInfo.funcs.Parts[j]) 1396 | } // if 1397 | } // else 1398 | } // for i 1399 | 1400 | for ; j0 < len(matB); j0++ { 1401 | if matB[j0] < 0 { 1402 | showInsWholeLine(newInfo.funcs.Parts[j0].oneLine()) 1403 | } 1404 | } 1405 | } 1406 | 1407 | func diff(orgInfo, newInfo *fileInfo) { 1408 | diffPackage(orgInfo, newInfo) 1409 | diffImports(orgInfo, newInfo) 1410 | diffTypes(orgInfo, newInfo) 1411 | diffVars(orgInfo, newInfo) 1412 | diffFuncs(orgInfo, newInfo) 1413 | } 1414 | 1415 | func readLines(fn villa.Path) []string { 1416 | bts, err := fn.ReadFile() 1417 | 1418 | if err != nil { 1419 | return nil 1420 | } 1421 | 1422 | return strings.Split(string(bts), "\n") 1423 | } 1424 | 1425 | // Options specifies options for processing files. 1426 | type Options struct { 1427 | NoColor bool // Turn off the colors when printing. 1428 | } 1429 | 1430 | var ( 1431 | gOut io.Writer 1432 | gOptions Options 1433 | ) 1434 | 1435 | // Exec prints the difference between two Go files to stdout. Not thread-safe. 1436 | func Exec(orgFn, newFn string, options Options) { 1437 | gOut = os.Stdout 1438 | gOptions = options 1439 | 1440 | fmtp.Printfln("Difference between %s and %s ...", orgFn, newFn) 1441 | 1442 | orgInfo, err1 := parse(orgFn, nil) 1443 | newInfo, err2 := parse(newFn, nil) 1444 | 1445 | if err1 != nil || err2 != nil { 1446 | orgLines := readLines(villa.Path(orgFn)) 1447 | newLines := readLines(villa.Path(newFn)) 1448 | 1449 | diffLines(orgLines, newLines, "%s") 1450 | return 1451 | } 1452 | 1453 | diff(orgInfo, newInfo) 1454 | } 1455 | 1456 | // ExecWriter prints the difference between two parsed Go files into w. Not thread-safe. 1457 | func ExecWriter(w io.Writer, fset0 *token.FileSet, file0 *ast.File, fset1 *token.FileSet, file1 *ast.File, options Options) { 1458 | gOut = w 1459 | gOptions = options 1460 | 1461 | orgInfo := &fileInfo{f: file0, fs: fset0} 1462 | orgInfo.collect() 1463 | newInfo := &fileInfo{f: file1, fs: fset1} 1464 | newInfo.collect() 1465 | 1466 | diff(orgInfo, newInfo) 1467 | } 1468 | --------------------------------------------------------------------------------